@oslokommune/punkt-elements
Version:
Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo
558 lines (440 loc) • 19.4 kB
text/typescript
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')
})
})
})