mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-02 11:18:57 +00:00
254 lines
7.9 KiB
TypeScript
254 lines
7.9 KiB
TypeScript
import { readFileSync } from 'node:fs'
|
|
|
|
import { createSSRApp, h } from 'vue'
|
|
import { renderToString } from 'vue/server-renderer'
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
import SkyField from '@/ui/controls/SkyField.vue'
|
|
|
|
describe('SkyField', () => {
|
|
it('forwards min, max, and step to the native input', async () => {
|
|
const app = createSSRApp(SkyField, {
|
|
ariaLabel: 'Primary frequency',
|
|
max: 999.9,
|
|
min: 0.1,
|
|
step: 0.1,
|
|
type: 'number',
|
|
})
|
|
|
|
const html = await renderToString(app)
|
|
const input = html.match(/<input[^>]+>/)?.[0] ?? ''
|
|
|
|
expect(input).toContain('aria-label="Primary frequency"')
|
|
expect(input).toContain('max="999.9"')
|
|
expect(input).toContain('min="0.1"')
|
|
expect(input).toContain('step="0.1"')
|
|
})
|
|
|
|
it('exposes the inline settings layout without changing input semantics', async () => {
|
|
const app = createSSRApp(SkyField, {
|
|
label: 'Service number',
|
|
layout: 'inline',
|
|
modelValue: '231',
|
|
})
|
|
|
|
const html = await renderToString(app)
|
|
|
|
expect(html).toContain('sky-field--inline')
|
|
expect(html).toMatch(/<label[^>]+for="([^"]+)"/)
|
|
expect(html).toContain('value="231"')
|
|
})
|
|
|
|
it('keeps the leading alias on the canonical Konsta media column', async () => {
|
|
const app = createSSRApp({
|
|
render: () =>
|
|
h(
|
|
SkyField,
|
|
{ label: 'Primary frequency' },
|
|
{ leading: () => h('span', 'icon') },
|
|
),
|
|
})
|
|
|
|
const html = await renderToString(app)
|
|
|
|
expect(html).toContain('sky-field--has-leading')
|
|
expect(html).toContain('sky-field--has-media')
|
|
expect(html).toContain('class="sky-field__media"')
|
|
expect(html.indexOf('sky-field__media')).toBeLessThan(
|
|
html.indexOf('sky-field__inner'),
|
|
)
|
|
})
|
|
|
|
it('renders canonical media, inner, label, and control nesting', async () => {
|
|
const app = createSSRApp({
|
|
render: () =>
|
|
h(
|
|
SkyField,
|
|
{ label: 'Name' },
|
|
{ media: () => h('img', { alt: 'icon', src: '/demo.png' }) },
|
|
),
|
|
})
|
|
|
|
const html = await renderToString(app)
|
|
|
|
expect(html).toMatch(
|
|
/<span class="sky-field__media">[\s\S]*?<\/span><div class="sky-field__inner"><label[\s\S]*?<div class="sky-field__control">/,
|
|
)
|
|
})
|
|
|
|
it('forwards text-entry hints to the native input', async () => {
|
|
const app = createSSRApp(SkyField, {
|
|
autocapitalize: 'none',
|
|
autocomplete: 'off',
|
|
autocorrect: 'off',
|
|
pattern: '[A-Z0-9]+',
|
|
spellcheck: false,
|
|
type: 'text',
|
|
})
|
|
|
|
const html = await renderToString(app)
|
|
const input = html.match(/<input[^>]+>/)?.[0] ?? ''
|
|
|
|
expect(input).toContain('autocapitalize="none"')
|
|
expect(input).toContain('autocomplete="off"')
|
|
expect(input).toContain('autocorrect="off"')
|
|
expect(input).toContain('pattern="[A-Z0-9]+"')
|
|
expect(input).toContain('spellcheck="false"')
|
|
})
|
|
|
|
it('renders a native date input without changing its accessible label', async () => {
|
|
const app = createSSRApp(SkyField, {
|
|
id: 'birthday',
|
|
label: 'Birthday',
|
|
modelValue: '2014-04-30',
|
|
type: 'date',
|
|
})
|
|
|
|
const html = await renderToString(app)
|
|
|
|
expect(html).toContain('<label class="sky-field__label" for="birthday"')
|
|
expect(html).toMatch(
|
|
/<input[^>]+id="birthday"[^>]+type="date"[^>]+value="2014-04-30"/,
|
|
)
|
|
})
|
|
|
|
it('renders typed select options and native options from the default slot', async () => {
|
|
const options = [
|
|
{ label: 'Male', value: 'male' },
|
|
{ disabled: true, label: 'Unavailable', value: 0 },
|
|
] as const
|
|
const app = createSSRApp({
|
|
render: () =>
|
|
h(
|
|
SkyField,
|
|
{
|
|
ariaLabel: 'Gender',
|
|
modelValue: 'male',
|
|
options,
|
|
placeholder: 'Please choose...',
|
|
type: 'select',
|
|
},
|
|
{
|
|
default: () => h('option', { value: 'custom' }, 'Custom'),
|
|
},
|
|
),
|
|
})
|
|
|
|
const html = await renderToString(app)
|
|
const select = html.match(/<select[^>]+>/)?.[0] ?? ''
|
|
|
|
expect(select).toContain('class="sky-field__input sky-field__select"')
|
|
expect(select).toContain('aria-label="Gender"')
|
|
expect(select).toContain('placeholder="Please choose..."')
|
|
expect(select).toContain('value="male"')
|
|
expect(html).not.toContain('>Please choose...</option>')
|
|
expect(html).toContain('<option value="male">Male</option>')
|
|
expect(html).toContain('<option disabled value="0">Unavailable</option>')
|
|
expect(html).toContain('<option value="custom">Custom</option>')
|
|
})
|
|
|
|
it('renders the exact Konsta dropdown triangle', async () => {
|
|
const app = createSSRApp(SkyField, {
|
|
dropdown: true,
|
|
options: [{ label: 'Male', value: 'Male' }],
|
|
type: 'select',
|
|
})
|
|
|
|
const html = await renderToString(app)
|
|
|
|
expect(html).toContain(
|
|
'<svg class="sky-field__dropdown" xmlns="http://www.w3.org/2000/svg" width="8" height="5" viewBox="0 0 8 5" fill="currentColor" aria-hidden="true" focusable="false">',
|
|
)
|
|
expect(html).toContain(
|
|
'<polygon fill-rule="evenodd" points="0 0 8 0 4 5"></polygon>',
|
|
)
|
|
})
|
|
|
|
it('raises floating labels only after the field has a value', async () => {
|
|
const emptyApp = createSSRApp(SkyField, {
|
|
floatingLabel: true,
|
|
label: 'Name',
|
|
placeholder: 'Your name',
|
|
})
|
|
const valuedApp = createSSRApp(SkyField, {
|
|
floatingLabel: true,
|
|
label: 'Name',
|
|
modelValue: 'Sky',
|
|
placeholder: 'Your name',
|
|
})
|
|
|
|
const emptyHtml = await renderToString(emptyApp)
|
|
const valuedHtml = await renderToString(valuedApp)
|
|
|
|
expect(emptyHtml).toContain('sky-field--floating-label')
|
|
expect(emptyHtml).not.toContain('sky-field--floating-raised')
|
|
expect(valuedHtml).toContain('sky-field--floating-label')
|
|
expect(valuedHtml).toContain('sky-field--floating-raised')
|
|
})
|
|
|
|
it('locks Konsta input, outline, floating-label, and reduced-motion geometry', () => {
|
|
const controls = readFileSync(
|
|
new URL('../controls.css', import.meta.url),
|
|
'utf8',
|
|
)
|
|
|
|
expect(controls).toMatch(
|
|
/\.sky-field__input\s*\{[^}]*height:\s*40px;[^}]*min-height:\s*40px;[^}]*font-size:\s*16px;[^}]*line-height:\s*24px/s,
|
|
)
|
|
expect(controls).toMatch(/\.sky-field__media,[\s\S]*?padding:\s*8px 0;/)
|
|
expect(controls).toMatch(
|
|
/\.sky-field__inner\s*\{[^}]*padding:\s*12px calc\(16px \+ var\(--sky-safe-area-right, 0px\)\) 12px 0/s,
|
|
)
|
|
expect(controls).toMatch(/\.sky-field--outline\s*\{[^}]*margin:\s*16px 0;/s)
|
|
expect(controls).toMatch(
|
|
/\.sky-field--floating-label:not\(\.sky-field--inline\) \.sky-field__label\s*\{[^}]*transform:\s*translateY\(16px\) scale\(1\.33\)/s,
|
|
)
|
|
expect(controls).toMatch(
|
|
/\.sky-field--floating-label\.sky-field--outline:not\([\s\S]*?\.sky-field__label\s*\{[^}]*transform:\s*translateY\(24px\) scale\(1\.33\)/s,
|
|
)
|
|
expect(controls).toMatch(
|
|
/@media \(prefers-reduced-motion: reduce\)\s*\{[^}]*\.sky-field__label/s,
|
|
)
|
|
})
|
|
|
|
it('renders an explicitly labelled clear control only for a non-empty value', async () => {
|
|
const app = createSSRApp(SkyField, {
|
|
clearButton: true,
|
|
clearLabel: 'Clear email',
|
|
modelValue: 'alex@example.com',
|
|
})
|
|
|
|
const html = await renderToString(app)
|
|
|
|
expect(html).toContain('class="sky-field__clear"')
|
|
expect(html).toContain('type="button"')
|
|
expect(html).toContain('aria-label="Clear email"')
|
|
})
|
|
|
|
it('does not expose an unnamed clear control', async () => {
|
|
const app = createSSRApp(SkyField, {
|
|
clearButton: true,
|
|
modelValue: 'alex@example.com',
|
|
})
|
|
|
|
const html = await renderToString(app)
|
|
|
|
expect(html).not.toContain('sky-field__clear')
|
|
})
|
|
|
|
it('declares its accessible clear event without changing existing events', () => {
|
|
const component = SkyField as unknown as { emits: string[] }
|
|
|
|
expect(component.emits).toEqual(
|
|
expect.arrayContaining([
|
|
'blur',
|
|
'change',
|
|
'clear',
|
|
'focus',
|
|
'input',
|
|
'update:modelValue',
|
|
]),
|
|
)
|
|
})
|
|
})
|