-
-
Notifications
You must be signed in to change notification settings - Fork 9.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #22518 from storybookjs/chaks/source-decorator-fea…
…t-fix Vue3: fix source decorator to generate proper story code (cherry picked from commit 6f0c2fc)
- Loading branch information
1 parent
15b6dc2
commit 639bd08
Showing
4 changed files
with
644 additions
and
297 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`); | ||
}); | ||
}); |
Oops, something went wrong.