-
Notifications
You must be signed in to change notification settings - Fork 84
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
Implement visual tests for KButton component #710
Changes from 5 commits
c6a3ab3
fe94675
05809de
855a5a2
d1ead28
798dd24
b84eafa
384e593
9471e37
6f3ab99
625cff0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,7 +7,17 @@ | |
Please do not modify the contents of this file. | ||
--> | ||
<div id="testing-playground" style="padding: 24px"> | ||
<component :is="component" v-bind="componentProps" /> | ||
<component :is="component" v-bind="componentProps"> | ||
<!-- Render default slot if provided --> | ||
<template v-if="slots.default"> | ||
<!-- eslint-disable-next-line vue/no-v-html --> | ||
<span v-html="slots.default"></span> | ||
</template> | ||
<!-- Render named slots passed to the component --> | ||
<template v-for="(slot, name) in namedSlots" #[name]> | ||
<component :is="slot.content" v-bind="slot.props" :key="name" /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be better if we follow the same pattern in both default slot and named slot, I would vote to render it inside a v-html. Or in any case, if rendering inside a div gives any error, we can be flexible and provide both options. If slots.name is String, then render it inside a v-html, if not, we can render it as you are doing it here. What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Won't the named slots be other components that are being nested in a particular component? In that case they'll always have a name and specific props attached to them just like the parent component, which is being handled correctly in the current scenario. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not necessarily, we can have html elements that are not vue components as part of named slots (e.g. this #divider in KListWithOverflow, It is a correct Vue syntax, there are actually no differences in the restrictions of what we can do in the default slot vs named slots. |
||
</template> | ||
</component> | ||
</div> | ||
|
||
</template> | ||
|
@@ -32,9 +42,25 @@ | |
* @type {Object} The props to be passed to the dynamically rendered component. | ||
*/ | ||
componentProps: {}, | ||
/** | ||
* @type {Object} The slots to be passed to the dynamically rendered component. | ||
*/ | ||
slots: {}, | ||
}; | ||
}, | ||
|
||
/** | ||
* Computed property that filters out the default slot from the slots object, | ||
* returning only the named slots. | ||
*/ | ||
computed: { | ||
namedSlots() { | ||
// eslint-disable-next-line no-unused-vars | ||
const { default: defaultSlot, ...rest } = this.slots; | ||
return rest; | ||
}, | ||
}, | ||
|
||
/** | ||
* Adds an event listener for messages from the test runner. | ||
* This listener will trigger the `handleMessage` method. | ||
|
@@ -53,12 +79,13 @@ | |
methods: { | ||
/** | ||
* Handles messages received from the test runner to render a specified component. | ||
* @param {MessageEvent} event - The message event containing the component and its props. | ||
* @param {MessageEvent} event - The message event containing the component and its props. | ||
*/ | ||
handleMessage(event) { | ||
if (event.data.type === 'RENDER_COMPONENT') { | ||
this.component = event.data.component; | ||
this.componentProps = event.data.props; | ||
this.slots = event.data.slots || {}; | ||
} | ||
}, | ||
}, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,23 +1,24 @@ | ||
import percySnapshot from '@percy/puppeteer'; | ||
|
||
export async function renderComponent(component, props) { | ||
export async function renderComponent(component, props, slots = {}) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here we need to add a jsdoc explaining these props, and explaining the structure slots should follow There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, will get it done 👍 |
||
const beforeRenderState = await page.evaluate(() => { | ||
const testing_playground = document.querySelector('#testing-playground'); | ||
return testing_playground ? testing_playground.innerHTML : ''; | ||
}); | ||
|
||
await page.evaluate( | ||
({ component, props }) => { | ||
({ component, props, slots }) => { | ||
window.postMessage( | ||
{ | ||
type: 'RENDER_COMPONENT', | ||
component: component, | ||
props: props, | ||
slots: slots, | ||
}, | ||
'*' | ||
); | ||
}, | ||
{ component, props } | ||
{ component, props, slots } | ||
); | ||
await page.waitForSelector('#testing-playground'); | ||
|
||
|
@@ -40,8 +41,30 @@ export async function renderComponent(component, props) { | |
global.expect(isComponentRendered).toBe(true); | ||
} | ||
|
||
export async function takeSnapshot(name) { | ||
export async function takeSnapshot(name, options = {}) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here would be good also to have some jsdoc, and let users know what options could we use (I think it should be a lot of options that percySnapshot receives, in any case we could link to their documentation). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure 👍 |
||
if (process.env.TEST_TYPE == 'visual') { | ||
await percySnapshot(page, name); | ||
await percySnapshot(page, name, options); | ||
} | ||
} | ||
|
||
export async function delay(time) { | ||
return new Promise(function(resolve) { | ||
setTimeout(resolve, time); | ||
}); | ||
} | ||
|
||
export const click = async selector => { | ||
await page.locator(selector).click(); | ||
}; | ||
|
||
export const hover = async selector => { | ||
await page.locator(selector).hover(); | ||
}; | ||
|
||
export const scrollToPos = async (selector, scrollOptions) => { | ||
await page.locator(selector).scroll(scrollOptions); | ||
}; | ||
|
||
export const waitFor = async selector => { | ||
await page.locator(selector).wait(); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,12 @@ | ||
import { shallowMount } from '@vue/test-utils'; | ||
import KButton from '../KButton.vue'; | ||
import { renderComponent, takeSnapshot } from '../../../jest.conf/visual.testUtils'; | ||
import { | ||
renderComponent, | ||
takeSnapshot, | ||
delay, | ||
click, | ||
hover, | ||
} from '../../../jest.conf/visual.testUtils'; | ||
|
||
describe('KButton', () => { | ||
describe('icon related props', () => { | ||
|
@@ -67,9 +73,94 @@ describe('KButton', () => { | |
}); | ||
|
||
describe.visual('KButton Visual Tests', () => { | ||
it('renders correctly with default props', async () => { | ||
await renderComponent('KButton', { text: 'Test Button' }); | ||
await takeSnapshot('KButton - Default'); | ||
it('renders correctly with different appearances', async () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO each snapshot should be a test case. Unit test cases should be simple and test just one thing. If we want to group multiple test cases like for example with the disable state, we could use a describe block inside de discribe.visual block and group them. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, if I am getting it correctly, for the case where we are testing different appearances of KButton for example, we need to write them all in separate There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, although Im not sure if we should group them as "different appearances", in general all snapshots are different appearances. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh different appearances means different values for the |
||
await renderComponent('KButton', { | ||
text: 'Raised Button', | ||
primary: true, | ||
appearance: 'raised-button', | ||
}); | ||
await takeSnapshot('KButton - Primary Raised Button', { widths: [400], minHeight: 512 }); | ||
|
||
await renderComponent('KButton', { | ||
text: 'Raised Button', | ||
primary: false, | ||
appearance: 'raised-button', | ||
}); | ||
await takeSnapshot('KButton - Secondary Raised Button', { widths: [400], minHeight: 512 }); | ||
|
||
await renderComponent('KButton', { | ||
text: 'Flat Button', | ||
primary: true, | ||
appearance: 'flat-button', | ||
}); | ||
await takeSnapshot('KButton - Primary Flat Button', { widths: [400], minHeight: 512 }); | ||
|
||
await renderComponent('KButton', { | ||
text: 'Flat Button', | ||
primary: false, | ||
appearance: 'flat-button', | ||
}); | ||
await takeSnapshot('KButton - Secondary Flat Button', { widths: [400], minHeight: 512 }); | ||
|
||
await renderComponent('KButton', { text: 'Basic Link', appearance: 'basic-link' }); | ||
await takeSnapshot('KButton - Basic Link', { widths: [400], minHeight: 512 }); | ||
}); | ||
it('renders correctly when disabled', async () => { | ||
await renderComponent('KButton', { | ||
text: 'Raised Button', | ||
disabled: true, | ||
appearance: 'raised-button', | ||
}); | ||
await takeSnapshot('KButton - Disabled Raised Button', { widths: [400], minHeight: 512 }); | ||
|
||
await renderComponent('KButton', { | ||
text: 'Flat Button', | ||
disabled: true, | ||
appearance: 'flat-button', | ||
}); | ||
await takeSnapshot('KButton - Disabled Flat Button', { widths: [400], minHeight: 512 }); | ||
}); | ||
it('renders with hover state', async () => { | ||
await renderComponent('KButton', { text: 'Raised Button', appearance: 'raised-button' }); | ||
await hover('button'); | ||
await delay(4000); | ||
await takeSnapshot('KButton - Raised Button Hover', { widths: [400], minHeight: 512 }); | ||
|
||
await renderComponent('KButton', { text: 'Flat Button', appearance: 'flat-button' }); | ||
await hover('button'); | ||
await delay(4000); | ||
await takeSnapshot('KButton - Flat Button Hover', { widths: [400], minHeight: 512 }); | ||
}); | ||
it('renders correctly with icon and iconAfter', async () => { | ||
await renderComponent('KButton', { text: 'Icon Button', icon: 'add' }); | ||
await takeSnapshot('KButton - With Icons', { widths: [400], minHeight: 512 }); | ||
|
||
await renderComponent('KButton', { text: 'Icon After', iconAfter: 'video' }); | ||
await takeSnapshot('KButton - With Icons After', { widths: [400], minHeight: 512 }); | ||
}); | ||
it('renders correctly with KDropdownMenu slot and shows options on click', async () => { | ||
await renderComponent( | ||
'KButton', | ||
{ text: 'Button with Dropdown' }, | ||
{ | ||
menu: { | ||
content: 'KDropdownMenu', | ||
props: { options: ['Option 1', 'Option 2'] }, | ||
}, | ||
} | ||
); | ||
await click('button'); | ||
await takeSnapshot('KButton - Dropdown Opened', { widths: [400], minHeight: 512 }); | ||
}); | ||
it('should render the slot when the slot has content', async () => { | ||
await renderComponent( | ||
'KButton', | ||
{ text: 'Button' }, | ||
{ | ||
default: '<span>Default Slot</span>', | ||
} | ||
); | ||
await takeSnapshot('KButton - With Default Slot', { widths: [400], minHeight: 512 }); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why should this be in as span? I think a div would be more appropiate in case we need to add more elements inside
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I take the KButton test with default slot as an example, if you use div, the button will become twice the size, with the upper part showing slot content and the lower part showing the content passed to button itself. That's the reason for using span here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm I see, but there could be some components where a span could also be a problem. What if we add something like a "wrapperEl" prop that defines the element container, and a "content" that defines the v-html prop? Any other idea is welcome 👐.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can combine both and have something like: