Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vue3: Fix source decorator to generate correct story code #22518

Merged
merged 13 commits into from
Jun 8, 2023
Merged
355 changes: 281 additions & 74 deletions code/renderers/vue3/src/docs/sourceDecorator.test.ts
@@ -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`);
});
});