mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-04 09:13:24 +00:00
Render the shared CSS chevron as a sized block so its declared geometry is applied and the icon sits centrally within the navbar back control. Extend the navbar contract test to protect the corrected layout.
402 lines
14 KiB
TypeScript
402 lines
14 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 SkyNavbar from '@/ui/SkyNavbar.vue'
|
|
import SkySegmented from '@/ui/controls/SkySegmented.vue'
|
|
import SkySegmentedButton from '@/ui/controls/SkySegmentedButton.vue'
|
|
|
|
import { getSkyNavbarCollapseState } from './navbar-collapse'
|
|
|
|
const foundationStyles = readFileSync(
|
|
new URL('./foundation.css', import.meta.url),
|
|
'utf8',
|
|
)
|
|
const controlsStyles = readFileSync(
|
|
new URL('./controls.css', import.meta.url),
|
|
'utf8',
|
|
)
|
|
const appPageSource = readFileSync(
|
|
new URL('./SkyAppPage.vue', import.meta.url),
|
|
'utf8',
|
|
)
|
|
const navbarSource = readFileSync(
|
|
new URL('./SkyNavbar.vue', import.meta.url),
|
|
'utf8',
|
|
)
|
|
|
|
describe('SkyNavbar', () => {
|
|
it('keeps the compact centered header as the default', async () => {
|
|
const html = await renderToString(
|
|
createSSRApp(SkyNavbar, { title: 'Account' }),
|
|
)
|
|
|
|
expect(html).toContain('sky-navbar--compact')
|
|
expect(html).toContain('sky-navbar__blur')
|
|
expect(html).toContain('sky-navbar__background')
|
|
expect(html).toContain('sky-navbar__inner')
|
|
expect(html).not.toContain('sky-navbar__left')
|
|
expect(html).not.toContain('sky-navbar__right')
|
|
expect(html).toContain('<h1 class="sky-navbar__title">')
|
|
expect(html).toContain('Account')
|
|
})
|
|
|
|
it('exposes the large-title header without changing heading semantics', async () => {
|
|
const html = await renderToString(
|
|
createSSRApp(SkyNavbar, {
|
|
title: 'Settings',
|
|
variant: 'large',
|
|
}),
|
|
)
|
|
|
|
expect(html).toContain('sky-navbar--large')
|
|
expect(html).toContain('sky-navbar--no-navigation')
|
|
expect(html).not.toContain('sky-navbar__inner')
|
|
expect(html).not.toContain('sky-navbar__left')
|
|
expect(html).not.toContain('sky-navbar__right')
|
|
expect(html).toContain('<h1 class="sky-navbar__title">')
|
|
expect(html).toContain('Settings')
|
|
})
|
|
|
|
it('keeps migrated boolean sizing and inner hooks compatible', async () => {
|
|
const html = await renderToString(
|
|
createSSRApp({
|
|
render: () =>
|
|
h(
|
|
SkyNavbar,
|
|
{
|
|
component: 'nav',
|
|
innerClass: 'custom-inner',
|
|
large: true,
|
|
title: 'Clock',
|
|
},
|
|
{ right: () => h('button', 'Edit') },
|
|
),
|
|
}),
|
|
)
|
|
|
|
expect(html).toMatch(/^<nav/)
|
|
expect(html).toContain('sky-navbar--large')
|
|
expect(html).toContain('sky-navbar__inner custom-inner')
|
|
})
|
|
|
|
it('renders the extended navigation row only when it has controls', async () => {
|
|
const html = await renderToString(
|
|
createSSRApp({
|
|
render: () =>
|
|
h(
|
|
SkyNavbar,
|
|
{ title: 'Account', variant: 'large' },
|
|
{ right: () => h('button', { type: 'button' }, 'Done') },
|
|
),
|
|
}),
|
|
)
|
|
|
|
expect(html).not.toContain('sky-navbar--no-navigation')
|
|
expect(html).toContain('sky-navbar__inner')
|
|
expect(html).not.toContain('sky-navbar__left')
|
|
expect(html).toContain('sky-navbar__right')
|
|
expect(html).toContain('>Done</button>')
|
|
})
|
|
|
|
it('reserves the navigation-row geometry without empty elements', () => {
|
|
const noNavigationRule = foundationStyles.match(
|
|
/\.sky-navbar--no-navigation\s*\{(?<declarations>[^}]*)\}/,
|
|
)?.groups?.declarations
|
|
const navigationRowRule = foundationStyles.match(
|
|
/\.sky-navbar__inner\s*\{(?<declarations>[^}]*)\}/,
|
|
)?.groups?.declarations
|
|
|
|
expect(noNavigationRule).toBeDefined()
|
|
expect(noNavigationRule).toContain('var(--sky-navbar-safe-area-top)')
|
|
expect(noNavigationRule).toContain('var(--sky-navbar-height)')
|
|
expect(noNavigationRule).toContain('var(--sky-space-3)')
|
|
expect(navigationRowRule).toBeDefined()
|
|
expect(navigationRowRule).toContain('margin-bottom: var(--sky-space-3)')
|
|
})
|
|
|
|
it('reserves the Konsta iOS title row below the navigation controls', () => {
|
|
const mediumNavbarRule = foundationStyles.match(
|
|
/(?:^|\n)\.sky-navbar--medium\s*\{(?<declarations>[^}]*)\}/,
|
|
)?.groups?.declarations
|
|
const largeNavbarRule = foundationStyles.match(
|
|
/(?:^|\n)\.sky-navbar--large\s*\{(?<declarations>[^}]*)\}/,
|
|
)?.groups?.declarations
|
|
|
|
expect(mediumNavbarRule).toContain('var(--sky-navbar-height)')
|
|
expect(mediumNavbarRule).not.toContain(
|
|
'var(--sky-navbar-large-title-height)',
|
|
)
|
|
expect(largeNavbarRule).toContain('var(--sky-navbar-height)')
|
|
expect(largeNavbarRule).toContain('var(--sky-navbar-large-title-height)')
|
|
})
|
|
|
|
it('uses Konsta medium and large collapse geometry', () => {
|
|
expect(getSkyNavbarCollapseState(0, 52)).toEqual({
|
|
compactTitleOpacity: 0,
|
|
extendedTitleOpacity: 1,
|
|
offset: 0,
|
|
})
|
|
expect(getSkyNavbarCollapseState(26, 52)).toEqual({
|
|
compactTitleOpacity: 0.25,
|
|
extendedTitleOpacity: 0,
|
|
offset: 26,
|
|
})
|
|
expect(getSkyNavbarCollapseState(100, 52)).toEqual({
|
|
compactTitleOpacity: 1,
|
|
extendedTitleOpacity: 0,
|
|
offset: 52,
|
|
})
|
|
expect(getSkyNavbarCollapseState(44, 44).offset).toBe(44)
|
|
})
|
|
|
|
it('fades a compact transparent title without collapsing its layout', () => {
|
|
expect(getSkyNavbarCollapseState(44, 44, false)).toEqual({
|
|
compactTitleOpacity: 1,
|
|
extendedTitleOpacity: 0,
|
|
offset: 0,
|
|
})
|
|
expect(navbarSource).toContain(
|
|
'hasExtendedTitle.value || props.transparent',
|
|
)
|
|
expect(foundationStyles).toMatch(
|
|
/\.sky-navbar--transparent \.sky-navbar__heading\s*\{[^}]*opacity:\s*var\(--sky-navbar-compact-title-opacity\)/s,
|
|
)
|
|
})
|
|
|
|
it('compensates the collapsing sibling header inside the scroll content', () => {
|
|
const scrollAreaRule = foundationStyles.match(
|
|
/(?:^|\n)\.sky-scroll-area\s*\{(?<declarations>[^}]*)\}/,
|
|
)?.groups?.declarations
|
|
const compensationRule = foundationStyles.match(
|
|
/(?:^|\n)\.sky-scroll-area::before\s*\{(?<declarations>[^}]*)\}/,
|
|
)?.groups?.declarations
|
|
|
|
expect(scrollAreaRule).toContain('overflow-anchor: none')
|
|
expect(compensationRule).toContain(
|
|
'height: var(--sky-page-collapse-offset)',
|
|
)
|
|
expect(appPageSource).toContain(
|
|
"'--sky-page-collapse-offset': `${pageScroll.collapseOffset.value}px`",
|
|
)
|
|
expect(navbarSource).toContain(
|
|
'pageScroll.collapseOffset.value = collapseState.value.offset',
|
|
)
|
|
})
|
|
|
|
it('keeps one semantic heading while exposing the collapsed visual title', async () => {
|
|
const html = await renderToString(
|
|
createSSRApp(SkyNavbar, { title: 'Store', variant: 'large' }),
|
|
)
|
|
|
|
expect(html.match(/<h1/g)).toHaveLength(1)
|
|
expect(html).toContain('sky-navbar__collapsed-heading')
|
|
expect(html).toContain('aria-hidden="true"')
|
|
})
|
|
|
|
it('keeps the custom title slot in sync while collapsed', async () => {
|
|
const html = await renderToString(
|
|
createSSRApp({
|
|
render: () =>
|
|
h(
|
|
SkyNavbar,
|
|
{ title: 'Fallback', variant: 'large' },
|
|
{ title: () => 'Custom title' },
|
|
),
|
|
}),
|
|
)
|
|
|
|
expect(html).not.toContain('Fallback')
|
|
expect(html.match(/Custom title/g)).toHaveLength(2)
|
|
expect(html.match(/<h1/g)).toHaveLength(1)
|
|
})
|
|
|
|
it('separates and fades the Konsta navbar blur before the scroll content', () => {
|
|
expect(foundationStyles).toMatch(
|
|
/\.sky-navbar__background\s*\{[\s\S]*?linear-gradient\([\s\S]*?--sky-navbar-glass[\s\S]*?transparent 100%/,
|
|
)
|
|
expect(foundationStyles).toMatch(
|
|
/@supports[\s\S]*?-webkit-mask-image: linear-gradient\(#000, transparent\)[\s\S]*?\.sky-navbar__blur\s*\{[\s\S]*?backdrop-filter: blur\(2px\)[\s\S]*?-webkit-mask-image: linear-gradient\([\s\S]*?#000 50%[\s\S]*?transparent 100%/,
|
|
)
|
|
expect(foundationStyles).not.toMatch(
|
|
/\.sky-navbar__background\s*\{[^}]*backdrop-filter/,
|
|
)
|
|
})
|
|
|
|
it('exposes the optional surface back affordance for detail screens', async () => {
|
|
const html = await renderToString(
|
|
createSSRApp(SkyNavbar, {
|
|
backAppearance: 'surface',
|
|
backLabel: 'Back to Settings',
|
|
showBack: true,
|
|
title: 'Account',
|
|
}),
|
|
)
|
|
|
|
expect(html).toContain('sky-navbar__back--surface')
|
|
expect(html).toContain('aria-label="Back to Settings"')
|
|
expect(html).toContain('class="sky-navbar__back-icon"')
|
|
expect(html).toContain('viewBox="0 0 12 20"')
|
|
expect(html).not.toContain('lucide-chevron-left')
|
|
})
|
|
|
|
it('keeps the Konsta glass wrappers and centers the exact iOS back icon', () => {
|
|
expect(navbarSource).toContain('class="sky-navbar__left sky-glass-surface"')
|
|
expect(navbarSource).toContain(
|
|
'class="sky-navbar__right sky-glass-surface"',
|
|
)
|
|
expect(foundationStyles).toMatch(
|
|
/\.sky-navbar__back\s*\{[^}]*padding:\s*0 16px;[^}]*justify-content:\s*center/s,
|
|
)
|
|
expect(foundationStyles).toMatch(
|
|
/\.sky-navbar__back-icon\s*\{[^}]*width:\s*12px;[^}]*height:\s*20px;[^}]*display:\s*block;[^}]*fill:\s*currentColor/s,
|
|
)
|
|
})
|
|
|
|
it('inherits the Konsta iOS foreground for navbar and toolbar actions', () => {
|
|
expect(foundationStyles).toMatch(
|
|
/\.sky-navbar__back\s*\{[^}]*color:\s*inherit;/s,
|
|
)
|
|
expect(controlsStyles).toMatch(
|
|
/\.sky-navbar \.sky-link,\s*\.sky-navbar \.sky-navbar-back-link,\s*\.sky-toolbar \.sky-link\s*\{[^}]*color:\s*inherit;/s,
|
|
)
|
|
expect(controlsStyles).toMatch(
|
|
/\.sky-link\s*\{[^}]*color:\s*var\(--sky-app-accent, #007aff\);/s,
|
|
)
|
|
expect(controlsStyles).toMatch(
|
|
/\.sky-navbar-back-link:active:not\(:disabled\)\s*\{[^}]*opacity:\s*0\.5;[^}]*transition-duration:\s*0ms;/s,
|
|
)
|
|
expect(controlsStyles).toMatch(
|
|
/\.sky-navbar-back-link__chevron\s*\{[^}]*width:\s*12px;[^}]*height:\s*12px;[^}]*display:\s*block;/s,
|
|
)
|
|
expect(controlsStyles).not.toMatch(
|
|
/\.sky-navbar-back-link:active:not\(:disabled\)\s*\{[^}]*--sky-app-accent-soft/s,
|
|
)
|
|
})
|
|
|
|
it('renders the Konsta 56px subnavbar and custom class hook', async () => {
|
|
const html = await renderToString(
|
|
createSSRApp({
|
|
render: () =>
|
|
h(
|
|
SkyNavbar,
|
|
{ subnavbarClass: 'custom-subnavbar', title: 'Search' },
|
|
{ subnavbar: () => h('label', 'Search controls') },
|
|
),
|
|
}),
|
|
)
|
|
const subnavbarRule = foundationStyles.match(
|
|
/(?:^|\n)\.sky-navbar__subnavbar\s*\{(?<declarations>[^}]*)\}/,
|
|
)?.groups?.declarations
|
|
|
|
expect(html).toContain('sky-navbar--with-subnavbar')
|
|
expect(html).toContain('sky-navbar__subnavbar')
|
|
expect(html).toContain('custom-subnavbar')
|
|
expect(subnavbarRule).toContain('height: 56px')
|
|
expect(subnavbarRule).toContain('var(--sky-safe-area-right)')
|
|
expect(subnavbarRule).toContain('var(--sky-safe-area-left)')
|
|
expect(subnavbarRule).not.toMatch(/padding-(?:top|bottom)|padding:\s/)
|
|
})
|
|
|
|
it('uses the Konsta navbar glass heights with and without subnavbar', () => {
|
|
const navbarRule = foundationStyles.match(
|
|
/(?:^|\n)\.sky-navbar\s*\{(?<declarations>[^}]*)\}/,
|
|
)?.groups?.declarations
|
|
const baseEffectRule = foundationStyles.match(
|
|
/\.sky-navbar__blur,\s*\.sky-navbar__background\s*\{(?<declarations>[^}]*)\}/,
|
|
)?.groups?.declarations
|
|
const subnavbarEffectRule = foundationStyles.match(
|
|
/\.sky-navbar--with-subnavbar \.sky-navbar__blur,\s*\.sky-navbar--with-subnavbar \.sky-navbar__background\s*\{(?<declarations>[^}]*)\}/,
|
|
)?.groups?.declarations
|
|
|
|
expect(navbarRule).toContain(
|
|
'--sky-navbar-safe-area-top: max(16px, var(--sky-safe-area-top))',
|
|
)
|
|
expect(baseEffectRule).toContain('var(--sky-navbar-safe-area-top)')
|
|
expect(baseEffectRule).toContain('var(--sky-navbar-height)')
|
|
expect(baseEffectRule).toContain('+ 16px')
|
|
expect(subnavbarEffectRule).toContain('var(--sky-navbar-safe-area-top)')
|
|
expect(subnavbarEffectRule).toContain('var(--sky-navbar-height)')
|
|
expect(subnavbarEffectRule).toContain('+ 70px + 16px')
|
|
expect(foundationStyles).toMatch(
|
|
/\.sky-navbar--outline::after\s*\{[^}]*bottom:\s*0/s,
|
|
)
|
|
expect(foundationStyles).not.toMatch(
|
|
/\.sky-navbar--with-subnavbar\.sky-navbar--outline[\s\S]*?\.sky-navbar__background::after/,
|
|
)
|
|
})
|
|
|
|
it('provides the Konsta navbar Glass context to segmented content', async () => {
|
|
const html = await renderToString(
|
|
createSSRApp({
|
|
render: () =>
|
|
h(
|
|
SkyNavbar,
|
|
{ title: 'Store' },
|
|
{
|
|
subnavbar: () =>
|
|
h(
|
|
SkySegmented,
|
|
{
|
|
activeIndex: 0,
|
|
ariaLabel: 'Store sections',
|
|
itemCount: 2,
|
|
strong: true,
|
|
},
|
|
{
|
|
default: () => [
|
|
h(SkySegmentedButton, { active: true }, () => 'Apps'),
|
|
h(SkySegmentedButton, { active: false }, () => 'Games'),
|
|
],
|
|
},
|
|
),
|
|
},
|
|
),
|
|
}),
|
|
)
|
|
|
|
expect(html).toContain('sky-navbar__subnavbar')
|
|
expect(html).toContain('sky-glass')
|
|
expect(html).toContain('sky-glass--highlight')
|
|
expect(html).toContain('sky-segmented--navbar')
|
|
expect(html).not.toContain('sky-segmented--navigation')
|
|
expect(html).toContain('sky-segmented__highlight')
|
|
expect(foundationStyles).toContain('height: 56px')
|
|
})
|
|
|
|
it('lets subnavbar content explicitly opt out of navigation Glass', async () => {
|
|
const html = await renderToString(
|
|
createSSRApp({
|
|
render: () =>
|
|
h(
|
|
SkyNavbar,
|
|
{ title: 'Filters' },
|
|
{
|
|
subnavbar: () =>
|
|
h(
|
|
SkySegmented,
|
|
{ ariaLabel: 'Filters', navigation: false },
|
|
{ default: () => h(SkySegmentedButton, () => 'All') },
|
|
),
|
|
},
|
|
),
|
|
}),
|
|
)
|
|
|
|
expect(html).toContain('sky-navbar__subnavbar')
|
|
expect(html).not.toContain('sky-glass')
|
|
expect(html).not.toContain('sky-segmented--navigation')
|
|
})
|
|
|
|
it('keeps Konsta transparent navbar glass while fading the compact title', () => {
|
|
expect(foundationStyles).toMatch(
|
|
/\.sky-navbar--transparent \.sky-navbar__heading\s*\{[^}]*opacity:\s*var\(--sky-navbar-compact-title-opacity\)/s,
|
|
)
|
|
expect(foundationStyles).not.toMatch(
|
|
/\.sky-navbar--transparent \.sky-navbar__(?:blur|background)\s*\{[^}]*opacity:\s*0/s,
|
|
)
|
|
})
|
|
})
|