Skip to content

Commit

Permalink
Merge pull request #22518 from storybookjs/chaks/source-decorator-fea…
Browse files Browse the repository at this point in the history
…t-fix

Vue3: fix source decorator to generate proper story code
(cherry picked from commit 6f0c2fc)
  • Loading branch information
chakAs3 authored and github-actions[bot] committed Jun 14, 2023
1 parent c4eeadc commit 2abf330
Show file tree
Hide file tree
Showing 4 changed files with 644 additions and 297 deletions.
355 changes: 281 additions & 74 deletions code/renderers/vue3/src/docs/sourceDecorator.test.ts
Original file line number Diff line number Diff line change
@@ -1,95 +1,302 @@
import { describe, expect, test } from '@jest/globals';
import type { Args } from '@storybook/types';
import { generateSource } from './sourceDecorator';

import type { ArgsType } from 'jest-mock';
import {
mapAttributesAndDirectives,
generateAttributesSource,
attributeSource,
htmlEventAttributeToVueEventAttribute as htmlEventToVueEvent,
} from './sourceDecorator';

expect.addSnapshotSerializer({
print: (val: any) => val,
test: (val: unknown) => typeof val === 'string',
});
function generateArgTypes(args: Args, slotProps: string[] | undefined) {
return Object.keys(args).reduce((acc, prop) => {
acc[prop] = { table: { category: slotProps?.includes(prop) ? 'slots' : 'props' } };
return acc;
}, {} as Record<string, any>);
}
function generateForArgs(args: Args, slotProps: string[] | undefined = undefined) {
return generateSource({ name: 'Component' }, args, generateArgTypes(args, slotProps), true);
}
function generateMultiComponentForArgs(args: Args, slotProps: string[] | undefined = undefined) {
return generateSource(
[{ name: 'Component' }, { name: 'Component' }],
args,
generateArgTypes(args, slotProps),
true
);
}

describe('generateSource Vue3', () => {
test('boolean true', () => {
expect(generateForArgs({ booleanProp: true })).toMatchInlineSnapshot(
`<Component :boolean-prop='booleanProp'/>`
);
describe('Vue3: sourceDecorator->mapAttributesAndDirective()', () => {
test('camelCase boolean Arg', () => {
expect(mapAttributesAndDirectives({ camelCaseBooleanArg: true })).toMatchInlineSnapshot(`
Array [
Object {
arg: Object {
content: camel-case-boolean-arg,
loc: Object {
source: camel-case-boolean-arg,
},
},
exp: Object {
isStatic: false,
loc: Object {
source: true,
},
},
loc: Object {
source: :camel-case-boolean-arg="true",
},
modifiers: Array [
,
],
name: bind,
type: 6,
},
]
`);
});
test('boolean false', () => {
expect(generateForArgs({ booleanProp: false })).toMatchInlineSnapshot(
`<Component :boolean-prop='booleanProp'/>`
);
test('camelCase string Arg', () => {
expect(mapAttributesAndDirectives({ camelCaseStringArg: 'foo' })).toMatchInlineSnapshot(`
Array [
Object {
arg: Object {
content: camel-case-string-arg,
loc: Object {
source: camel-case-string-arg,
},
},
exp: Object {
isStatic: false,
loc: Object {
source: foo,
},
},
loc: Object {
source: camel-case-string-arg="foo",
},
modifiers: Array [
,
],
name: bind,
type: 6,
},
]
`);
});
test('null property', () => {
expect(generateForArgs({ nullProp: null })).toMatchInlineSnapshot(
`<Component :null-prop='nullProp'/>`
);
test('boolean arg', () => {
expect(mapAttributesAndDirectives({ booleanarg: true })).toMatchInlineSnapshot(`
Array [
Object {
arg: Object {
content: booleanarg,
loc: Object {
source: booleanarg,
},
},
exp: Object {
isStatic: false,
loc: Object {
source: true,
},
},
loc: Object {
source: :booleanarg="true",
},
modifiers: Array [
,
],
name: bind,
type: 6,
},
]
`);
});
test('string property', () => {
expect(generateForArgs({ stringProp: 'mystr' })).toMatchInlineSnapshot(
`<Component :string-prop='stringProp'/>`
);
test('string arg', () => {
expect(mapAttributesAndDirectives({ stringarg: 'bar' })).toMatchInlineSnapshot(`
Array [
Object {
arg: Object {
content: stringarg,
loc: Object {
source: stringarg,
},
},
exp: Object {
isStatic: false,
loc: Object {
source: bar,
},
},
loc: Object {
source: stringarg="bar",
},
modifiers: Array [
,
],
name: bind,
type: 6,
},
]
`);
});
test('number property', () => {
expect(generateForArgs({ numberProp: 42 })).toMatchInlineSnapshot(
`<Component :number-prop='numberProp'/>`
);
test('number arg', () => {
expect(mapAttributesAndDirectives({ numberarg: 2023 })).toMatchInlineSnapshot(`
Array [
Object {
arg: Object {
content: numberarg,
loc: Object {
source: numberarg,
},
},
exp: Object {
isStatic: false,
loc: Object {
source: 2023,
},
},
loc: Object {
source: :numberarg="2023",
},
modifiers: Array [
,
],
name: bind,
type: 6,
},
]
`);
});
test('object property', () => {
expect(generateForArgs({ objProp: { x: true } })).toMatchInlineSnapshot(
`<Component :obj-prop='objProp'/>`
);
test('camelCase boolean, string, and number Args', () => {
expect(
mapAttributesAndDirectives({
camelCaseBooleanArg: true,
camelCaseStringArg: 'foo',
cameCaseNumberArg: 2023,
})
).toMatchInlineSnapshot(`
Array [
Object {
arg: Object {
content: camel-case-boolean-arg,
loc: Object {
source: camel-case-boolean-arg,
},
},
exp: Object {
isStatic: false,
loc: Object {
source: true,
},
},
loc: Object {
source: :camel-case-boolean-arg="true",
},
modifiers: Array [
,
],
name: bind,
type: 6,
},
Object {
arg: Object {
content: camel-case-string-arg,
loc: Object {
source: camel-case-string-arg,
},
},
exp: Object {
isStatic: false,
loc: Object {
source: foo,
},
},
loc: Object {
source: camel-case-string-arg="foo",
},
modifiers: Array [
,
],
name: bind,
type: 6,
},
Object {
arg: Object {
content: came-case-number-arg,
loc: Object {
source: came-case-number-arg,
},
},
exp: Object {
isStatic: false,
loc: Object {
source: 2023,
},
},
loc: Object {
source: :came-case-number-arg="2023",
},
modifiers: Array [
,
],
name: bind,
type: 6,
},
]
`);
});
test('multiple properties', () => {
expect(generateForArgs({ a: 1, b: 2 })).toMatchInlineSnapshot(`<Component :a='a' :b='b'/>`);
});

describe('Vue3: sourceDecorator->generateAttributesSource()', () => {
test('camelCase boolean Arg', () => {
expect(
generateAttributesSource(
mapAttributesAndDirectives({ camelCaseBooleanArg: true }),
{ camelCaseBooleanArg: true },
[{ camelCaseBooleanArg: { type: 'boolean' } }] as ArgsType<Args>
)
).toMatchInlineSnapshot(`:camel-case-boolean-arg="true"`);
});
test('1 slot property', () => {
expect(generateForArgs({ content: 'xyz', myProp: 'abc' }, ['content'])).toMatchInlineSnapshot(`
<Component :my-prop='myProp'>
{{ content }}
</Component>
`);
test('camelCase string Arg', () => {
expect(
generateAttributesSource(
mapAttributesAndDirectives({ camelCaseStringArg: 'foo' }),
{ camelCaseStringArg: 'foo' },
[{ camelCaseStringArg: { type: 'string' } }] as ArgsType<Args>
)
).toMatchInlineSnapshot(`camel-case-string-arg="foo"`);
});
test('multiple slot property with second slot value not set', () => {
expect(generateForArgs({ content: 'xyz', myProp: 'abc' }, ['content', 'footer']))
.toMatchInlineSnapshot(`
<Component :my-prop='myProp'>
{{ content }}
</Component>
`);

test('camelCase boolean, string, and number Args', () => {
expect(
generateAttributesSource(
mapAttributesAndDirectives({
camelCaseBooleanArg: true,
camelCaseStringArg: 'foo',
cameCaseNumberArg: 2023,
}),
{
camelCaseBooleanArg: true,
camelCaseStringArg: 'foo',
cameCaseNumberArg: 2023,
},
[] as ArgsType<Args>
)
).toMatchInlineSnapshot(
`:camel-case-boolean-arg="true" camel-case-string-arg="foo" :came-case-number-arg="2023"`
);
});
test('multiple slot property with second slot value is set', () => {
expect(generateForArgs({ content: 'xyz', footer: 'foo', myProp: 'abc' }, ['content', 'footer']))
.toMatchInlineSnapshot(`
<Component :my-prop='myProp'>
<template #content>{{ content }}</template>
<template #footer>{{ footer }}</template>
</Component>
`);
});

describe('Vue3: sourceDecorator->attributeSoure()', () => {
test('camelCase boolean Arg', () => {
expect(attributeSource('stringArg', 'foo')).toMatchInlineSnapshot(`stringArg="foo"`);
});
// test mutil components
test('multi component with boolean true', () => {
expect(generateMultiComponentForArgs({ booleanProp: true })).toMatchInlineSnapshot(`
<Component :boolean-prop='booleanProp'/>
<Component :boolean-prop='booleanProp'/>
`);

test('html event attribute should convert to vue event directive', () => {
expect(attributeSource('onClick', () => {})).toMatchInlineSnapshot(`v-on:click='()=>({})'`);
expect(attributeSource('onclick', () => {})).toMatchInlineSnapshot(`v-on:click='()=>({})'`);
});
test('normal html attribute should not convert to vue event directive', () => {
expect(attributeSource('on-click', () => {})).toMatchInlineSnapshot(`on-click='()=>({})'`);
});
test('component is not set', () => {
expect(generateSource(null, {}, {})).toBeNull();
test('htmlEventAttributeToVueEventAttribute onEv => v-on:', () => {
const htmlEventAttributeToVueEventAttribute = (attribute: string) => {
return htmlEventToVueEvent(attribute);
};
expect(/^on[A-Za-z]/.test('onClick')).toBeTruthy();
expect(htmlEventAttributeToVueEventAttribute('onclick')).toMatchInlineSnapshot(`v-on:click`);
expect(htmlEventAttributeToVueEventAttribute('onClick')).toMatchInlineSnapshot(`v-on:click`);
expect(htmlEventAttributeToVueEventAttribute('onChange')).toMatchInlineSnapshot(`v-on:change`);
expect(htmlEventAttributeToVueEventAttribute('onFocus')).toMatchInlineSnapshot(`v-on:focus`);
expect(htmlEventAttributeToVueEventAttribute('on-focus')).toMatchInlineSnapshot(`on-focus`);
});
});
Loading

0 comments on commit 2abf330

Please sign in to comment.