diff --git a/lib/__tests__/__snapshots__/transform.js.snap b/lib/__tests__/__snapshots__/transform.js.snap index b5b0195..49b63a4 100644 --- a/lib/__tests__/__snapshots__/transform.js.snap +++ b/lib/__tests__/__snapshots__/transform.js.snap @@ -141,6 +141,57 @@ foo ==========" `; +exports[`classic components handles \`data-test-*\` boolean correctly 1`] = ` +"========== + + export default Component.extend({ + 'data-test-one': true, + 'data-test-two': true, + 'data-test-three': false, + }); + +~~~~~~~~~~ +foo +~~~~~~~~~~ + => tagName: div +~~~~~~~~~~ + + export default Component.extend({ + tagName: \\"\\" + }); + +~~~~~~~~~~ +
+ foo +
+==========" +`; + +exports[`classic components handles \`data-test-*\` literals correctly 1`] = ` +"========== + + export default Component.extend({ + 'data-test-item': 1, + 'data-test-name': 'Zoey', + }); + +~~~~~~~~~~ +foo +~~~~~~~~~~ + => tagName: div +~~~~~~~~~~ + + export default Component.extend({ + tagName: \\"\\" + }); + +~~~~~~~~~~ +
+ foo +
+==========" +`; + exports[`classic components handles \`elementId\` correctly 1`] = ` "========== @@ -189,6 +240,33 @@ foo ==========" `; +exports[`classic components handles \`hasTestSelectors\` option correctly 1`] = ` +"========== + + export default Component.extend({ + 'data-test-item': 1, + 'data-test-name': 'Zoey', + }); + +~~~~~~~~~~ +foo +~~~~~~~~~~ + => tagName: div +~~~~~~~~~~ + + export default Component.extend({ + tagName: \\"\\", + 'data-test-item': 1, + 'data-test-name': 'Zoey' + }); + +~~~~~~~~~~ +
+ foo +
+==========" +`; + exports[`classic components handles single \`classNames\` item correctly 1`] = ` "========== diff --git a/lib/__tests__/transform.js b/lib/__tests__/transform.js index d92d0e7..debab96 100644 --- a/lib/__tests__/transform.js +++ b/lib/__tests__/transform.js @@ -122,6 +122,60 @@ describe('classic components', function() { expect(generateSnapshot(source, template)).toMatchSnapshot(); }); + test('handles `data-test-*` boolean correctly', () => { + let source = ` + export default Component.extend({ + 'data-test-one': true, + 'data-test-two': true, + 'data-test-three': false, + }); + `; + + let template = `foo`; + + expect(generateSnapshot(source, template, { hasTestSelectors: true })).toMatchSnapshot(); + }); + + test('handles `data-test-*` literals correctly', () => { + let source = ` + export default Component.extend({ + 'data-test-item': 1, + 'data-test-name': 'Zoey', + }); + `; + + let template = `foo`; + + expect(generateSnapshot(source, template, { hasTestSelectors: true })).toMatchSnapshot(); + }); + + test('throws if data-test-* is a computed value', () => { + let source = ` + export default Component.extend({ + 'data-test-computed': computed(function() { + return true; + }), + }); + `; + + expect(() => + transform(source, '', { hasTestSelectors: true }) + ).toThrowErrorMatchingInlineSnapshot(`"Unsupported value for 'data-test-computed'"`); + }); + + test('handles `hasTestSelectors` option correctly', () => { + let source = ` + export default Component.extend({ + 'data-test-item': 1, + 'data-test-name': 'Zoey', + }); + `; + + let template = `foo`; + + expect(generateSnapshot(source, template, { hasTestSelectors: false })).toMatchSnapshot(); + }); + test('throws if `Component.extend({ ... })` argument is not found', () => { let source = ` export default Component.extend(); diff --git a/lib/index.js b/lib/index.js index 4fff6e8..c5f5ade 100644 --- a/lib/index.js +++ b/lib/index.js @@ -44,10 +44,13 @@ async function runForGlobs(patterns, cwd, options = {}) { let hasComponentCSS = (pkg.dependencies && pkg.dependencies['ember-component-css']) || (pkg.devDependencies && pkg.devDependencies['ember-component-css']); + let hasTestSelectors = + (pkg.dependencies && pkg.dependencies['ember-test-selectors']) || + (pkg.devDependencies && pkg.devDependencies['ember-test-selectors']); for (let path of paths) { try { - let tagName = transformPath(path, { hasComponentCSS }); + let tagName = transformPath(path, { hasComponentCSS, hasTestSelectors }); if (tagName) { log(chalk.green(`${chalk.dim(path)}: <${tagName}>...`)); } else { diff --git a/lib/transform/classic.js b/lib/transform/classic.js index aea07e0..0fd13b3 100644 --- a/lib/transform/classic.js +++ b/lib/transform/classic.js @@ -11,6 +11,8 @@ const { findAriaRole, isMethod, isProperty, + isStartsWithProperty, + findDataTestAttributes, } = require('../utils/classic'); const EVENT_HANDLER_METHODS = [ @@ -125,6 +127,9 @@ module.exports = function transformClassicComponent(root, options) { let classNameBindings = findClassNameBindings(properties); debug('classNameBindings: %o', classNameBindings); + let dataTestAttributes = findDataTestAttributes(properties); + debug('dataTestAttributes: %o', dataTestAttributes); + let ariaRole; try { ariaRole = findAriaRole(properties); @@ -155,6 +160,7 @@ module.exports = function transformClassicComponent(root, options) { isProperty(path, 'attributeBindings') || isProperty(path, 'classNames') || isProperty(path, 'classNameBindings') || + (options.hasTestSelectors && isStartsWithProperty(path, 'data-test-')) || isProperty(path, 'ariaRole') ) .remove(); @@ -168,6 +174,7 @@ module.exports = function transformClassicComponent(root, options) { classNames, classNameBindings, attributeBindings, + dataTestAttributes, ariaRole, }; }; diff --git a/lib/transform/native.js b/lib/transform/native.js index 566dbf1..7b442ac 100644 --- a/lib/transform/native.js +++ b/lib/transform/native.js @@ -141,6 +141,15 @@ module.exports = function transformNativeComponent(root, options) { .forEach(path => removeDecorator(root, path, 'className', '@ember-decorators/component')); let newSource = root.toSource(); - - return { newSource, tagName, elementId, classNames, classNameBindings, attributeBindings }; + let dataTestAttributes = new Map(); + + return { + newSource, + tagName, + elementId, + classNames, + classNameBindings, + attributeBindings, + dataTestAttributes, + }; }; diff --git a/lib/transform/template.js b/lib/transform/template.js index dbc4706..47a0b85 100644 --- a/lib/transform/template.js +++ b/lib/transform/template.js @@ -8,7 +8,15 @@ const PLACEHOLDER = '@@@PLACEHOLDER@@@'; module.exports = function transformTemplate( template, - { tagName, elementId, classNames, classNameBindings, attributeBindings, ariaRole }, + { + tagName, + elementId, + classNames, + classNameBindings, + attributeBindings, + ariaRole, + dataTestAttributes, + }, options ) { // wrap existing template with root element @@ -50,6 +58,11 @@ module.exports = function transformTemplate( attrs.push(b.attr('class', b.concat(parts))); } + if (options.hasTestSelectors) { + dataTestAttributes.forEach((value, key) => { + attrs.push(b.attr(key, b.text(value))); + }); + } attrs.push(b.attr('...attributes', b.text(''))); let templateAST = templateRecast.parse(template); diff --git a/lib/utils/classic.js b/lib/utils/classic.js index dd55803..a1de16d 100644 --- a/lib/utils/classic.js +++ b/lib/utils/classic.js @@ -7,6 +7,18 @@ function isProperty(path, name) { return node.type === 'ObjectProperty' && node.key.type === 'Identifier' && node.key.name === name; } +function isStartsWithProperty(path, name) { + let node = path.value; + if (node.type === 'ObjectProperty') { + if (node.key.type === 'Identifier') { + return node.key.name.startsWith(name); + } + if (node.key.type === 'StringLiteral') { + return node.key.value.startsWith(name); + } + } +} + function isMethod(path, name) { let node = path.value; return node.type === 'ObjectMethod' && node.key.type === 'Identifier' && node.key.name === name; @@ -91,8 +103,38 @@ function findClassNameBindings(properties) { return classNameBindings; } +function findDataTestAttributes(properties) { + let nodes = properties.filter(path => isStartsWithProperty(path, 'data-test-')); + if (!nodes) { + return []; + } + const mappedProperties = nodes.map(node => { + return { + name: node.value.key.value, + value: node.value.value.value, + valueType: node.value.value.type, + }; + }); + + let dataTestAttributes = new Map(); + for (let { name, value, valueType } of mappedProperties) { + if (valueType === 'BooleanLiteral') { + if (value) { + dataTestAttributes.set(name, null); + } + } else if (valueType === 'NumericLiteral' || valueType === 'StringLiteral') { + dataTestAttributes.set(name, String(value)); + } else { + throw new SilentError(`Unsupported value for '${name}'`); + } + } + + return dataTestAttributes; +} + module.exports = { isProperty, + isStartsWithProperty, isMethod, findStringProperty, findStringArrayProperties, @@ -100,6 +142,7 @@ module.exports = { findAttributeBindings, findClassNames, findClassNameBindings, + findDataTestAttributes, findElementId, findTagName, };