Skip to content
This repository has been archived by the owner on Jul 12, 2019. It is now read-only.

Commit

Permalink
fix(icons-vue): update render and add tests (#383)
Browse files Browse the repository at this point in the history
* fix: icons-vue class and style attributes

* fix: icons-vue class and style attributes part 2

* fix: icons-vue class and style attrutes - remove console.log

* fix: icons-vue class and style attrutes - minimise diff

* chore(icons-vue): add initial test setup for createFromInfo

* chore: update pr tests and fix aria-label

* fix(icons-vue): add title support and update tests

* chore(icons-vue): remove unused variable

* chore: fix class test and remove style story
  • Loading branch information
lee-chase authored and joshblack committed Mar 5, 2019
1 parent 2628301 commit 4b4822e
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 46 deletions.
Binary file removed .yarn-offline-mirror/rollup-0.67.3.tgz
Binary file not shown.
Binary file added .yarn-offline-mirror/vue-2.6.8.tgz
Binary file not shown.
4 changes: 3 additions & 1 deletion packages/icons-vue/package.json
Expand Up @@ -30,7 +30,9 @@
"fs-extra": "^7.0.1",
"prettier": "^1.15.2",
"rimraf": "^2.6.2",
"rollup": "^0.67.3"
"rollup": "^0.66.6",
"rollup-plugin-virtual": "^1.0.1",
"vue": "^2.6.8"
},
"sideEffects": false
}
210 changes: 210 additions & 0 deletions packages/icons-vue/src/__tests__/createFromInfo-test.js
@@ -0,0 +1,210 @@
/**
* Copyright IBM Corp. 2018, 2018
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*
* @jest-environment jsdom
*/

'use strict';

const Module = require('module');
const rollup = require('rollup').rollup;
const virtual = require('rollup-plugin-virtual');
const vm = require('vm');
// Include version of Vue that has built-in template support
const Vue = require('vue/dist/vue');

async function getModuleFromString(
string,
{ external = ['@carbon/icon-helpers'], name = '<MockIconModule>' } = {}
) {
const bundle = await rollup({
input: '__entrypoint__',
external,
plugins: [
virtual({
__entrypoint__: string,
}),
],
});
const { code } = await bundle.generate({
format: 'cjs',
});
const sandbox = {
console,
module: new Module(name),
require,
};
vm.createContext(sandbox);
vm.runInContext(code, sandbox);

return sandbox.module.exports;
}

describe('createFromInfo', () => {
describe('createModuleFromInfo', () => {
let createModuleFromInfo;
let descriptor;
let info;
let mountNode;
let MockIconComponent;
let render;

beforeEach(async () => {
createModuleFromInfo = require('../createFromInfo').createModuleFromInfo;

descriptor = {
attrs: {
width: 16,
height: 16,
viewBox: '0 0 16 16',
},
content: [
{
elem: 'circle',
attrs: {
cx: 8,
cy: 8,
r: 8,
},
},
],
};

info = {
descriptor,
moduleName: 'MockIcon',
};

mountNode = document.createElement('div');
document.body.appendChild(mountNode);

MockIconComponent = await getModuleFromString(createModuleFromInfo(info));

render = ({ components, template, ...rest }) => {
const rootNode = mountNode.appendChild(document.createElement('div'));

new Vue({
el: rootNode,
components,
template,
...rest,
});

// Vue ends up replacing `rootNode` so we need to use the `mountNode` to
// look for the last `<MockIcon />` added to the DOM, most likely this
// is the last <svg> node that has been inserted
return mountNode.querySelector('svg:last-of-type');
};
});

afterEach(() => {
mountNode.parentNode.removeChild(mountNode);
});

it('should create a renderable component', async () => {
const originalConsoleError = console.error;
console.error = jest.fn();

render({
components: {
[MockIconComponent.name]: MockIconComponent,
},
template: `<MockIcon />`,
});

// Verify that we can render without errors
expect(console.error).not.toHaveBeenCalled();

console.error = originalConsoleError;
});

it('should treat focusable as a string', async () => {
const defaultNode = render({
components: {
[MockIconComponent.name]: MockIconComponent,
},
template: `<MockIcon />`,
});
const focusableNode = render({
components: {
[MockIconComponent.name]: MockIconComponent,
},
template: `<MockIcon focusable="true" />`,
});

expect(defaultNode.getAttribute('focusable')).toBe('false');
expect(focusableNode.getAttribute('focusable')).toBe('true');
});

it('should support rendering a title in the SVG markup', async () => {
const node = render({
components: {
[MockIconComponent.name]: MockIconComponent,
},
template: `<MockIcon tabindex="0" title="Custom title" />`,
});

const children = Array.from(node.children);
expect(children[0].tagName).toBe('title');

for (let i = 1; i < descriptor.content.length; i++) {
// We do i + 1 here since 0 is used for title above
expect(children[i].tagName).toBe(descriptor.content[i].elem);
}
});

it('should support custom class names', async () => {
const customClass = 'foo';
const dynamicClass = 'bar';
const node = render({
components: {
[MockIconComponent.name]: MockIconComponent,
},
data() {
return {
myDynamicClass: dynamicClass,
};
},
template: `<MockIcon class="${customClass}" v-bind:class="myDynamicClass" />`,
});

expect(node.classList.contains(customClass)).toBe(true);
expect(node.classList.contains(dynamicClass)).toBe(true);
});

it('should be focusable if aria-label and tabindex is used', async () => {
const label = 'custom-label';
const node = render({
components: {
[MockIconComponent.name]: MockIconComponent,
},
template: `<MockIcon aria-label="${label}" tabindex="0" />`,
});

expect(node.getAttribute('aria-label')).toBe(label);
expect(node.getAttribute('role')).toBe('img');
expect(node.getAttribute('tabindex')).toBe('0');
expect(node.getAttribute('focusable')).toBe('true');
});

it('should create a clickable component', async () => {
const onClick = jest.fn();
const node = render({
components: {
[MockIconComponent.name]: MockIconComponent,
},
data: {
onClick,
},
template: `<MockIcon aria-label="custom-label" tabindex="0" v-on:click="onClick" />`,
});

node.dispatchEvent(new MouseEvent('click'));

expect(onClick).toHaveBeenCalledTimes(1);
});
});
});
65 changes: 30 additions & 35 deletions packages/icons-vue/src/createFromInfo.js
Expand Up @@ -47,51 +47,46 @@ export default ${createComponentFromInfo(info)};`;
}

function createComponentFromInfo(info) {
const { descriptor, moduleName, size } = info;
const { descriptor, moduleName } = info;
const { attrs, content } = descriptor;
return `{
name: '${moduleName}',
functional: true,
props: [
'ariaLabel',
'ariaLabelledBy',
'height',
'title',
'viewBox',
'width',
'preserveAspectRatio',
'tabindex',
'xmlns',
],
// We use title as the prop name as it is not a valid attribute for an SVG
// HTML element
props: ['title'],
render(createElement, context) {
const { props, listeners, slots } = context;
const {
ariaLabel,
ariaLabelledBy,
width = '${attrs.width}',
height = '${attrs.height}',
viewBox = '${attrs.viewBox}',
preserveAspectRatio = 'xMidYMid meet',
xmlns = 'http://www.w3.org/2000/svg',
...rest
} = props;
const { children, data, listeners, props } = context;
const attrs = getAttributes({
...rest,
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledBy,
width,
height,
viewBox,
preserveAspectRatio,
xmlns,
width: '${attrs.width}',
height: '${attrs.height}',
viewBox: '${attrs.viewBox}',
preserveAspectRatio: 'xMidYMid meet',
xmlns: 'http://www.w3.org/2000/svg',
// Special case here, we need to coordinate that we are using title,
// potentially, to get the right focus attributes
title: props.title,
...data.attrs,
});
return createElement('svg', {
const svgData = {
attrs,
on: listeners,
}, [
slots.title,
};
if (data.staticClass) {
svgData.class = {
[data.staticClass]: true,
};
}
if (data.class) {
svgData.class[data.class] = true;
}
return createElement('svg', svgData, [
props.title && createElement('title', null, props.title),
${content.map(toString).join(', ')},
slots.default,
children,
]);
},
};`;
Expand Down
4 changes: 2 additions & 2 deletions packages/icons-vue/src/createIconStory.js
Expand Up @@ -50,11 +50,11 @@ storiesOf('${moduleName}', module)
action: action('clicked'),
},
}))
.add('with custom class', () => ({
.add('with custom classes', () => ({
components: {
icon: ${moduleName},
},
template: \`<icon class="custom class"></icon>\`,
template: \`<icon class="custom classes" :class="'dynamic classes_2'"></icon>\`,
}));`;
}

Expand Down
13 changes: 5 additions & 8 deletions yarn.lock
Expand Up @@ -9751,14 +9751,6 @@ rollup@^0.67.1:
"@types/estree" "0.0.39"
"@types/node" "*"

rollup@^0.67.3:
version "0.67.3"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.67.3.tgz#55475b1b62c43220c3b4bd7edc5846233932f50b"
integrity sha512-TyNQCz97rKuVVbsKUTXfwIjV7UljWyTVd7cTMuE+aqlQ7WJslkYF5QaYGjMLR2BlQtUOO5CAxSVnpQ55iYp5jg==
dependencies:
"@types/estree" "0.0.39"
"@types/node" "*"

rsvp@^3.3.3:
version "3.6.2"
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.2.tgz#2e96491599a96cde1b515d5674a8f7a91452926a"
Expand Down Expand Up @@ -11151,6 +11143,11 @@ vfile@^3.0.0:
unist-util-stringify-position "^1.0.0"
vfile-message "^1.0.0"

vue@^2.6.8:
version "2.6.8"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.8.tgz#f21cbc536bfc14f7d1d792a137bb12f69e60ea91"
integrity sha512-+vp9lEC2Kt3yom673pzg1J7T1NVGuGzO9j8Wxno+rQN2WYVBX2pyo/RGQ3fXCLh2Pk76Skw/laAPCuBuEQ4diw==

w3c-hr-time@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045"
Expand Down

0 comments on commit 4b4822e

Please sign in to comment.