8f1b1fb5cd
* feat(ui): replace generic building icon with company monogram in user menu The company row and switcher flyout in the sidebar user menu showed lucide Building2 for every company. Render the company's initial in a small rounded square instead (square = company, circle = person), so each company gets a mark of its own. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(mobile): touch behavior polish for the mobile web experience - kill -webkit-tap-highlight-color flash; touch-action: manipulation on interactive elements (no double-tap-to-zoom wait); user-select: none on buttons (long-press no longer enters text selection) - overscroll-behavior-y: contain on html/body: pull-to-refresh no longer hijacks list scrolling, inner scrollers stop chaining to the document (contain, not none, so iOS rubber-banding survives) - 16px font-size floor for form fields on coarse pointers: iOS Safari stops zooming into focused inputs; desktop keeps text-sm - min-h-screen -> min-h-dvh everywhere: correct height under collapsing mobile browser chrome, identical on desktop - active: variants mirror hover: on Button: Tailwind 4 gates hover: behind (hover: hover), so touch devices previously got zero pointer feedback - theme-color now tracks the app: default was a leftover blue #304D83; SSR emits white and ThemeColorSync mirrors the computed --background into the meta tag across dark mode and palette switches Hover-stuck-after-tap and viewport-fit/safe-area were already covered (Tailwind 4 hover gating; existing viewportFit: cover + safe-area utilities). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: log monogram and overscroll decisions Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
149 lines
5.9 KiB
TypeScript
149 lines
5.9 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
|
|
const ENV_KEYS = [
|
|
'NEXT_PUBLIC_BRANDING_APP_NAME',
|
|
'NEXT_PUBLIC_BRANDING_APP_DESCRIPTION',
|
|
'BRANDING_LEGAL_ENTITY',
|
|
'BRANDING_SUPPORT_EMAIL',
|
|
'BRANDING_PRIVACY_EMAIL',
|
|
'BRANDING_SECURITY_EMAIL',
|
|
'NEXT_PUBLIC_BRANDING_AUTH_EMAIL_FROM',
|
|
'NEXT_PUBLIC_BRANDING_LOGO_PATH',
|
|
'NEXT_PUBLIC_BRANDING_FAVICON_PATH',
|
|
'NEXT_PUBLIC_BRANDING_APPLE_ICON_PATH',
|
|
'NEXT_PUBLIC_BRANDING_PWA_ICON_BASE',
|
|
'NEXT_PUBLIC_BRANDING_THEME_COLOR',
|
|
'NEXT_PUBLIC_BRANDING_MANIFEST_THEME_COLOR',
|
|
'NEXT_PUBLIC_BRANDING_MANIFEST_BG_COLOR',
|
|
'NEXT_PUBLIC_BRANDING_HIDDEN_NAV',
|
|
'NEXT_PUBLIC_BRANDING_NAV_DENSITY',
|
|
] as const
|
|
|
|
describe('branding service', () => {
|
|
const originalEnv: Record<string, string | undefined> = {}
|
|
|
|
beforeEach(() => {
|
|
for (const key of ENV_KEYS) {
|
|
originalEnv[key] = process.env[key]
|
|
delete process.env[key]
|
|
}
|
|
})
|
|
|
|
afterEach(async () => {
|
|
for (const key of ENV_KEYS) {
|
|
if (originalEnv[key] === undefined) delete process.env[key]
|
|
else process.env[key] = originalEnv[key]
|
|
}
|
|
const { registerBrandingService } = await import('../service')
|
|
registerBrandingService({})
|
|
})
|
|
|
|
it('returns accounted defaults when nothing is overridden', async () => {
|
|
const { getBranding } = await import('../service')
|
|
const b = getBranding()
|
|
expect(b.appName).toBe('Accounted')
|
|
expect(b.appDescription).toBe('Ekonomihantering')
|
|
expect(b.legalEntity).toBe('Arcim Technology AB')
|
|
expect(b.supportEmail).toBe('support@gnubok.se')
|
|
expect(b.privacyEmail).toBe('privacy@gnubok.se')
|
|
expect(b.securityEmail).toBe('security@arcim.io')
|
|
expect(b.authEmailFrom).toBe('noreply@gnubok.se')
|
|
expect(b.logoPath).toBe('/accounted-icon.png')
|
|
expect(b.faviconPath).toBe('/favicon.ico')
|
|
expect(b.appleTouchIconPath).toBe('/icons/icon-192.png')
|
|
expect(b.pwaIconBasePath).toBe('/icons')
|
|
expect(b.themeColor).toBe('#ffffff')
|
|
expect(b.manifestThemeColor).toBe('#1a1a1a')
|
|
expect(b.manifestBackgroundColor).toBe('#ffffff')
|
|
expect(b.hiddenNavHrefs).toEqual([])
|
|
expect(b.navDensity).toBe('standard')
|
|
})
|
|
|
|
it('accepts NEXT_PUBLIC_BRANDING_NAV_DENSITY=slim', async () => {
|
|
process.env.NEXT_PUBLIC_BRANDING_NAV_DENSITY = 'slim'
|
|
const { getBranding } = await import('../service')
|
|
expect(getBranding().navDensity).toBe('slim')
|
|
})
|
|
|
|
it('ignores invalid NEXT_PUBLIC_BRANDING_NAV_DENSITY values', async () => {
|
|
process.env.NEXT_PUBLIC_BRANDING_NAV_DENSITY = 'compact'
|
|
const { getBranding } = await import('../service')
|
|
expect(getBranding().navDensity).toBe('standard')
|
|
})
|
|
|
|
it('extension override can set navDensity', async () => {
|
|
const { getBranding, registerBrandingService } = await import('../service')
|
|
registerBrandingService({ navDensity: 'slim' })
|
|
expect(getBranding().navDensity).toBe('slim')
|
|
})
|
|
|
|
it('parses NEXT_PUBLIC_BRANDING_HIDDEN_NAV as comma-separated hrefs', async () => {
|
|
process.env.NEXT_PUBLIC_BRANDING_HIDDEN_NAV = '/salary,/salary/employees,/customers'
|
|
const { getBranding } = await import('../service')
|
|
expect(getBranding().hiddenNavHrefs).toEqual(['/salary', '/salary/employees', '/customers'])
|
|
})
|
|
|
|
it('trims whitespace and drops empty entries in hidden nav list', async () => {
|
|
process.env.NEXT_PUBLIC_BRANDING_HIDDEN_NAV = ' /salary , ,/customers, '
|
|
const { getBranding } = await import('../service')
|
|
expect(getBranding().hiddenNavHrefs).toEqual(['/salary', '/customers'])
|
|
})
|
|
|
|
it('empty NEXT_PUBLIC_BRANDING_HIDDEN_NAV keeps default empty list', async () => {
|
|
process.env.NEXT_PUBLIC_BRANDING_HIDDEN_NAV = ''
|
|
const { getBranding } = await import('../service')
|
|
expect(getBranding().hiddenNavHrefs).toEqual([])
|
|
})
|
|
|
|
it('extension override replaces hiddenNavHrefs', async () => {
|
|
process.env.NEXT_PUBLIC_BRANDING_HIDDEN_NAV = '/salary'
|
|
const { getBranding, registerBrandingService } = await import('../service')
|
|
registerBrandingService({ hiddenNavHrefs: ['/customers', '/suppliers'] })
|
|
expect(getBranding().hiddenNavHrefs).toEqual(['/customers', '/suppliers'])
|
|
})
|
|
|
|
it('env vars override defaults', async () => {
|
|
process.env.NEXT_PUBLIC_BRANDING_APP_NAME = 'Holdio'
|
|
process.env.BRANDING_SUPPORT_EMAIL = 'hello@holdio.se'
|
|
process.env.NEXT_PUBLIC_BRANDING_LOGO_PATH = '/holdio-logo.svg'
|
|
process.env.NEXT_PUBLIC_BRANDING_AUTH_EMAIL_FROM = 'noreply@holdio.se'
|
|
const { getBranding } = await import('../service')
|
|
const b = getBranding()
|
|
expect(b.appName).toBe('Holdio')
|
|
expect(b.supportEmail).toBe('hello@holdio.se')
|
|
expect(b.logoPath).toBe('/holdio-logo.svg')
|
|
expect(b.authEmailFrom).toBe('noreply@holdio.se')
|
|
expect(b.appDescription).toBe('Ekonomihantering')
|
|
})
|
|
|
|
it('extension override beats env vars', async () => {
|
|
process.env.NEXT_PUBLIC_BRANDING_APP_NAME = 'EnvName'
|
|
const { getBranding, registerBrandingService } = await import('../service')
|
|
registerBrandingService({ appName: 'ExtensionName' })
|
|
expect(getBranding().appName).toBe('ExtensionName')
|
|
})
|
|
|
|
it('extension partial override leaves untouched fields at default', async () => {
|
|
const { getBranding, registerBrandingService } = await import('../service')
|
|
registerBrandingService({ appName: 'Holdio' })
|
|
const b = getBranding()
|
|
expect(b.appName).toBe('Holdio')
|
|
expect(b.legalEntity).toBe('Arcim Technology AB')
|
|
expect(b.supportEmail).toBe('support@gnubok.se')
|
|
})
|
|
|
|
it('empty string env var does not override', async () => {
|
|
process.env.NEXT_PUBLIC_BRANDING_APP_NAME = ''
|
|
const { getBranding } = await import('../service')
|
|
expect(getBranding().appName).toBe('Accounted')
|
|
})
|
|
|
|
it('clearing extension override returns to env/default resolution', async () => {
|
|
const { getBranding, registerBrandingService } = await import('../service')
|
|
registerBrandingService({ appName: 'Holdio' })
|
|
expect(getBranding().appName).toBe('Holdio')
|
|
registerBrandingService({})
|
|
expect(getBranding().appName).toBe('Accounted')
|
|
})
|
|
})
|