UNPKG

@oslokommune/punkt-elements

Version:

Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo

558 lines (440 loc) 19.4 kB
import '@testing-library/jest-dom' import { axe, toHaveNoViolations } from 'jest-axe' import { vi } from 'vitest' import { createElementTest, BaseTestConfig } from '../../tests/test-framework' import { CustomElementFor } from '../../tests/component-registry' import './card' expect.extend(toHaveNoViolations) export interface CardTestConfig extends BaseTestConfig { skin?: string layout?: string padding?: string borderOnHover?: boolean heading?: string subheading?: string headingLevel?: number href?: string linkText?: string image?: string imageAlt?: string tags?: string metaPrefix?: string metaLabel?: string metaDate?: string } // Use shared framework export const createCardTest = async (config: CardTestConfig = {}) => { const { container, element } = await createElementTest< CustomElementFor<'pkt-card'>, CardTestConfig >('pkt-card', config) return { container, card: element, } } // Cleanup after each test afterEach(() => { document.body.innerHTML = '' }) // Global console.warn spy to suppress validation warnings in tests let consoleWarnSpy: any beforeEach(() => { consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) }) afterEach(() => { if (consoleWarnSpy) { consoleWarnSpy.mockRestore() } }) describe('PktCard', () => { describe('Rendering and basic functionality', () => { test('renders without errors', async () => { const { card } = await createCardTest() expect(card).toBeInTheDocument() await card.updateComplete expect(card).toBeTruthy() const article = card.querySelector('article') expect(article).toBeInTheDocument() expect(article).toHaveClass('pkt-card') }) test('renders content in slot', async () => { const { card } = await createCardTest({ content: '<p>Test content here</p>' }) await card.updateComplete const content = card.querySelector('.pkt-card__content') expect(content).toBeInTheDocument() expect(content?.textContent).toContain('Test content here') }) test('renders basic structure correctly', async () => { const { card } = await createCardTest({ heading: 'Test Heading', content: 'Test content' }) await card.updateComplete const article = card.querySelector('article') const wrapper = article?.querySelector('.pkt-card__wrapper') const header = wrapper?.querySelector('.pkt-card__header') const content = wrapper?.querySelector('.pkt-card__content') expect(wrapper).toBeInTheDocument() expect(header).toBeInTheDocument() expect(content).toBeInTheDocument() }) }) describe('Properties and attributes', () => { test('applies default properties correctly', async () => { const { card } = await createCardTest() await card.updateComplete expect(card.skin).toBe('outlined') expect(card.layout).toBe('vertical') expect(card.padding).toBe('default') expect(card.borderOnHover).toBe(true) expect(card.tagPosition).toBe('top') expect(card.imageShape).toBe('square') expect(card.openLinkInNewTab).toBe(false) expect(card.headinglevel).toBe(3) const article = card.querySelector('article') expect(article).toHaveClass('pkt-card--outlined') expect(article).toHaveClass('pkt-card--vertical') expect(article).toHaveClass('pkt-card--padding-default') expect(article).toHaveClass('pkt-card--border-on-hover') }) test('applies different skin properties correctly', async () => { const skins = ['outlined', 'outlined-beige', 'gray', 'beige', 'green', 'blue'] for (const skin of skins) { const { card } = await createCardTest({ skin }) await card.updateComplete expect(card.skin).toBe(skin) expect(card.getAttribute('skin')).toBe(skin) const article = card.querySelector('article') expect(article).toHaveClass(`pkt-card--${skin}`) } }) test('rejects unsupported skin values and falls back to default', async () => { const unsupportedSkins = ['zebra', 'goldenrod', 'hotpink', 'rainbow', 'invalid'] for (const invalidSkin of unsupportedSkins) { const { card } = await createCardTest({ skin: invalidSkin }) await card.updateComplete // The component should now validate skin values and fall back to default expect(card.skin).not.toBe(invalidSkin) expect(card.skin).toBe('outlined') // Should fall back to default const article = card.querySelector('article') // Should not have the invalid CSS class expect(article).not.toHaveClass(`pkt-card--${invalidSkin}`) // Should have the default skin class instead expect(article).toHaveClass('pkt-card--outlined') } }) test('validates skin values and logs warnings for invalid skins', async () => { // Clear the global spy and create a new one for this specific test consoleWarnSpy.mockRestore() const localConsoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) const { card } = await createCardTest({ skin: 'zebra' }) await card.updateComplete // Should have logged a warning with the correct default value from spec expect(localConsoleSpy).toHaveBeenCalledWith( 'Invalid skin value "zebra". Using default skin "outlined".', ) // Should fall back to default from spec expect(card.skin).toBe('outlined') // Restore and recreate global spy for subsequent tests localConsoleSpy.mockRestore() consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) }) test('applies different layout properties correctly', async () => { const layouts = ['vertical', 'horizontal'] for (const layout of layouts) { const { card } = await createCardTest({ layout }) await card.updateComplete expect(card.layout).toBe(layout) expect(card.getAttribute('layout')).toBe(layout) const article = card.querySelector('article') expect(article).toHaveClass(`pkt-card--${layout}`) } }) test('applies different padding properties correctly', async () => { const paddingOptions = ['none', 'default'] for (const padding of paddingOptions) { const { card } = await createCardTest({ padding }) await card.updateComplete expect(card.padding).toBe(padding) expect(card.getAttribute('padding')).toBe(padding) const article = card.querySelector('article') expect(article).toHaveClass(`pkt-card--padding-${padding}`) } }) test('handles borderOnHover property correctly', async () => { // Test with borderOnHover false const { card } = await createCardTest() card.borderOnHover = false await card.updateComplete expect(card.borderOnHover).toBe(false) const article = card.querySelector('article') expect(article).not.toHaveClass('pkt-card--border-on-hover') }) }) describe('Heading functionality', () => { test('renders heading when provided', async () => { const { card } = await createCardTest({ heading: 'Test Card Title' }) await card.updateComplete expect(card.heading).toBe('Test Card Title') const heading = card.querySelector('pkt-heading') expect(heading).toBeInTheDocument() expect(heading).toHaveClass('pkt-card__heading') expect(heading?.textContent?.trim()).toBe('Test Card Title') }) test('renders subheading when provided', async () => { const { card } = await createCardTest({ subheading: 'Test Subheading' }) await card.updateComplete expect(card.subheading).toBe('Test Subheading') const subheading = card.querySelector('.pkt-card__subheading') expect(subheading).toBeInTheDocument() expect(subheading?.textContent).toBe('Test Subheading') }) test('applies correct heading level', async () => { const { card } = await createCardTest({ heading: 'Test', headingLevel: 2 }) await card.updateComplete expect(card.headinglevel).toBe(2) const heading = card.querySelector('pkt-heading') expect(heading?.getAttribute('level')).toBe('2') }) test('does not render header when no heading or subheading', async () => { const { card } = await createCardTest() await card.updateComplete const header = card.querySelector('.pkt-card__header') expect(header).not.toBeInTheDocument() }) }) describe('Link functionality', () => { test('renders as regular card when no clickCardLink', async () => { const { card } = await createCardTest({ heading: 'Test Title' }) await card.updateComplete expect(card.clickCardLink).toBe(null) const heading = card.querySelector('pkt-heading') const link = card.querySelector('.pkt-card__link') expect(heading).toBeInTheDocument() expect(link).not.toBeInTheDocument() const article = card.querySelector('article') expect(article?.getAttribute('aria-label')).toBe('Test Title') }) test('renders as link card when clickCardLink provided', async () => { const { card } = await createCardTest({ heading: 'Test Title' }) card.clickCardLink = '/test-url' await card.updateComplete expect(card.clickCardLink).toBe('/test-url') const linkHeading = card.querySelector('.pkt-card__link-heading') const link = card.querySelector('.pkt-card__link') expect(linkHeading).toBeInTheDocument() expect(link).toBeInTheDocument() expect(link?.getAttribute('href')).toBe('/test-url') expect(link?.textContent).toBe('Test Title') const article = card.querySelector('article') expect(article?.getAttribute('aria-label')).toBe('Test Title lenkekort') }) test('handles openLinkInNewTab correctly', async () => { const { card } = await createCardTest({ heading: 'Test' }) card.clickCardLink = '/test' card.openLinkInNewTab = true await card.updateComplete expect(card.openLinkInNewTab).toBe(true) const link = card.querySelector('.pkt-card__link') expect(link?.getAttribute('target')).toBe('_blank') }) test('applies correct aria-label for link cards', async () => { const { card } = await createCardTest() card.clickCardLink = '/test' card.ariaLabel = 'Custom aria label' await card.updateComplete const article = card.querySelector('article') expect(article?.getAttribute('aria-label')).toBe('Custom aria label') }) }) describe('Image functionality', () => { test('renders image when provided', async () => { const { card } = await createCardTest() card.image = { src: '/test-image.jpg', alt: 'Test image' } await card.updateComplete expect(card.image.src).toBe('/test-image.jpg') expect(card.image.alt).toBe('Test image') const imageDiv = card.querySelector('.pkt-card__image') const img = imageDiv?.querySelector('img') expect(imageDiv).toBeInTheDocument() expect(img).toBeInTheDocument() expect(img?.getAttribute('src')).toBe('/test-image.jpg') expect(img?.getAttribute('alt')).toBe('Test image') }) test('does not render image when not provided', async () => { const { card } = await createCardTest() await card.updateComplete const imageDiv = card.querySelector('.pkt-card__image') expect(imageDiv).not.toBeInTheDocument() }) test('applies correct image shape classes', async () => { const shapes = ['square', 'round'] as const for (const shape of shapes) { const { card } = await createCardTest() card.image = { src: '/test.jpg', alt: 'Test' } card.imageShape = shape await card.updateComplete expect(card.imageShape).toBe(shape) const imageDiv = card.querySelector('.pkt-card__image') expect(imageDiv).toHaveClass(`pkt-card__image-${shape}`) } }) }) describe('Tags functionality', () => { test('renders tags when provided', async () => { const { card } = await createCardTest() card.tags = [ { text: 'Tag 1', skin: 'blue' }, { text: 'Tag 2', skin: 'green' }, ] await card.updateComplete expect(card.tags).toHaveLength(2) const tagsContainer = card.querySelector('.pkt-card__tags') const tags = tagsContainer?.querySelectorAll('pkt-tag') expect(tagsContainer).toBeInTheDocument() expect(tags).toHaveLength(2) expect(tagsContainer?.getAttribute('aria-label')).toBe('merkelapper') }) test('renders single tag with correct aria-label', async () => { const { card } = await createCardTest() card.tags = [{ text: 'Single Tag' }] await card.updateComplete const tagsContainer = card.querySelector('.pkt-card__tags') expect(tagsContainer?.getAttribute('aria-label')).toBe('merkelapp') }) test('applies correct tag position classes', async () => { const positions = ['top', 'bottom'] as const for (const position of positions) { const { card, container } = await createCardTest() card.tags = [{ text: 'Test Tag' }] card.tagPosition = position await card.updateComplete expect(card.tagPosition).toBe(position) const tagsContainer = card.querySelector('.pkt-card__tags') expect(tagsContainer).toHaveClass(`pkt-card__tags-${position}`) // Cleanup for next iteration container.remove() } }) test('does not render tags when array is empty', async () => { const { card } = await createCardTest() await card.updateComplete const tagsContainer = card.querySelector('.pkt-card__tags') expect(tagsContainer).not.toBeInTheDocument() }) }) describe('Metadata functionality', () => { test('renders metadata when provided', async () => { const { card } = await createCardTest() card.metaLead = 'Author Name' card.metaTrail = '2023-12-01' await card.updateComplete expect(card.metaLead).toBe('Author Name') expect(card.metaTrail).toBe('2023-12-01') const metadata = card.querySelector('.pkt-card__metadata') const metaLead = metadata?.querySelector('.pkt-card__metadata-lead') const metaTrail = metadata?.querySelector('.pkt-card__metadata-trail') expect(metadata).toBeInTheDocument() expect(metaLead).toBeInTheDocument() expect(metaTrail).toBeInTheDocument() expect(metaLead?.textContent).toBe('Author Name') expect(metaTrail?.textContent).toBe('2023-12-01') }) test('renders only metaLead when metaTrail not provided', async () => { const { card } = await createCardTest() card.metaLead = 'Author Only' await card.updateComplete const metadata = card.querySelector('.pkt-card__metadata') const metaLead = metadata?.querySelector('.pkt-card__metadata-lead') const metaTrail = metadata?.querySelector('.pkt-card__metadata-trail') expect(metadata).toBeInTheDocument() expect(metaLead).toBeInTheDocument() expect(metaTrail).not.toBeInTheDocument() expect(metaLead?.textContent).toBe('Author Only') }) test('renders only metaTrail when metaLead not provided', async () => { const { card } = await createCardTest() card.metaTrail = 'Date Only' await card.updateComplete const metadata = card.querySelector('.pkt-card__metadata') const metaLead = metadata?.querySelector('.pkt-card__metadata-lead') const metaTrail = metadata?.querySelector('.pkt-card__metadata-trail') expect(metadata).toBeInTheDocument() expect(metaLead).not.toBeInTheDocument() expect(metaTrail).toBeInTheDocument() expect(metaTrail?.textContent).toBe('Date Only') }) test('does not render metadata when neither provided', async () => { const { card } = await createCardTest() await card.updateComplete const metadata = card.querySelector('.pkt-card__metadata') expect(metadata).not.toBeInTheDocument() }) }) describe('Content placement and structure', () => { test('renders content elements in correct order', async () => { const { card } = await createCardTest({ heading: 'Test Title', subheading: 'Test Sub', }) card.tags = [{ text: 'Test Tag' }] card.image = { src: '/test.jpg', alt: 'Test' } card.metaLead = 'Author' card.metaTrail = 'Date' await card.updateComplete const article = card.querySelector('article') const children = Array.from(article?.children || []) // Should have image first, then wrapper expect(children[0]).toHaveClass('pkt-card__image') expect(children[1]).toHaveClass('pkt-card__wrapper') const wrapper = children[1] const wrapperChildren = Array.from(wrapper?.children || []) // Order within wrapper: tags (top), header, content, metadata expect(wrapperChildren[0]).toHaveClass('pkt-card__tags-top') expect(wrapperChildren[1]).toHaveClass('pkt-card__header') expect(wrapperChildren[2]).toHaveClass('pkt-card__content') expect(wrapperChildren[3]).toHaveClass('pkt-card__metadata') }) test('places tags at bottom when tagPosition is bottom', async () => { const { card } = await createCardTest({ heading: 'Test Title' }) card.tags = [{ text: 'Test Tag' }] card.tagPosition = 'bottom' await card.updateComplete const wrapper = card.querySelector('.pkt-card__wrapper') const wrapperChildren = Array.from(wrapper?.children || []) // Order: header, content, tags (bottom) expect(wrapperChildren[0]).toHaveClass('pkt-card__header') expect(wrapperChildren[1]).toHaveClass('pkt-card__content') expect(wrapperChildren[2]).toHaveClass('pkt-card__tags-bottom') }) }) describe('Accessibility', () => { test('has no accessibility violations', async () => { const { card } = await createCardTest({ heading: 'Accessible Card', }) await card.updateComplete await card.updateComplete const results = await axe(card) expect(results).toHaveNoViolations() }) test('applies correct ARIA attributes', async () => { const { card } = await createCardTest({ heading: 'Test' }) card.ariaLabel = 'Custom accessible label' await card.updateComplete expect(card.ariaLabel).toBe('Custom accessible label') const article = card.querySelector('article') expect(article?.getAttribute('aria-label')).toBe('Custom accessible label') }) test('falls back to heading for aria-label when no explicit aria-label', async () => { const { card } = await createCardTest({ heading: 'Default Aria Label' }) await card.updateComplete const article = card.querySelector('article') expect(article?.getAttribute('aria-label')).toBe('Default Aria Label') }) test('falls back to "kort" when no heading or aria-label', async () => { const { card } = await createCardTest() await card.updateComplete const article = card.querySelector('article') expect(article?.getAttribute('aria-label')).toBe('kort') }) }) })