From e074aeb199e5bbb97ace187fdd5c62466b7c080c Mon Sep 17 00:00:00 2001 From: ncoronel Date: Fri, 26 Jun 2026 16:25:31 -0300 Subject: [PATCH] feat: implement tenant branding injection and SSR bootstrap logic with comprehensive testing --- src/app/app.spec.ts | 16 +++++++++++ src/app/app.ts | 28 ++++++++++++++++++++ src/app/core/services/tenant.service.spec.ts | 6 ++++- src/styles.scss | 6 +++++ 4 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/app/app.spec.ts b/src/app/app.spec.ts index 60c2626..72268d3 100644 --- a/src/app/app.spec.ts +++ b/src/app/app.spec.ts @@ -45,11 +45,27 @@ describe('App', () => { }).compileComponents(); const fixture = TestBed.createComponent(App); + fixture.detectChanges(); expect(fixture.componentInstance).toBeTruthy(); expect(fixture.nativeElement.style.getPropertyValue('--tenant-primary')).toBe( tenant.primary_color ); + expect(fixture.nativeElement.style.getPropertyValue('--tenant-secondary')).toBe( + tenant.secondary_color + ); + expect(fixture.nativeElement.style.getPropertyValue('--tenant-danger')).toBe( + tenant.danger_color + ); + expect(fixture.nativeElement.style.getPropertyValue('--tenant-primary-rgb')).toBe( + '99, 118, 243' + ); + expect(fixture.nativeElement.style.getPropertyValue('--tenant-secondary-rgb')).toBe( + '160, 160, 160' + ); + expect(fixture.nativeElement.style.getPropertyValue('--tenant-danger-rgb')).toBe( + '255, 136, 136' + ); }); it('renders the tenant not found screen when the tenant is missing', async () => { diff --git a/src/app/app.ts b/src/app/app.ts index 50f30a1..56774b9 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -4,6 +4,21 @@ import { RouterOutlet } from '@angular/router'; import { TenantService } from './core/services/tenant.service'; import { DEFAULT_TENANT_BRANDING } from './core/services/tenant-ssr-cache.store'; +function hexToRgb(hex: string): string { + const cleanHex = hex.replace('#', '').trim(); + let r = 0, g = 0, b = 0; + if (cleanHex.length === 3) { + r = parseInt(cleanHex[0] + cleanHex[0], 16); + g = parseInt(cleanHex[1] + cleanHex[1], 16); + b = parseInt(cleanHex[2] + cleanHex[2], 16); + } else if (cleanHex.length >= 6) { + r = parseInt(cleanHex.slice(0, 2), 16); + g = parseInt(cleanHex.slice(2, 4), 16); + b = parseInt(cleanHex.slice(4, 6), 16); + } + return `${r}, ${g}, ${b}`; +} + @Component({ selector: 'app-root', imports: [RouterOutlet], @@ -13,6 +28,9 @@ import { DEFAULT_TENANT_BRANDING } from './core/services/tenant-ssr-cache.store' '[style.--tenant-primary]': 'tenantPrimaryColor()', '[style.--tenant-secondary]': 'tenantSecondaryColor()', '[style.--tenant-danger]': 'tenantDangerColor()', + '[style.--tenant-primary-rgb]': 'tenantPrimaryColorRgb()', + '[style.--tenant-secondary-rgb]': 'tenantSecondaryColorRgb()', + '[style.--tenant-danger-rgb]': 'tenantDangerColorRgb()', '[style.--tenant-header-footer-bg]': 'tenantHeaderFooterBgColor()' } }) @@ -30,9 +48,19 @@ export class App { protected readonly tenantDangerColor = computed( () => this.tenantService.tenant()?.danger_color ?? DEFAULT_TENANT_BRANDING.dangerColor ); + protected readonly tenantPrimaryColorRgb = computed(() => + hexToRgb(this.tenantPrimaryColor()) + ); + protected readonly tenantSecondaryColorRgb = computed(() => + hexToRgb(this.tenantSecondaryColor()) + ); + protected readonly tenantDangerColorRgb = computed(() => + hexToRgb(this.tenantDangerColor()) + ); protected readonly tenantHeaderFooterBgColor = computed( () => this.tenantService.tenant()?.header_footer_bg_color ?? DEFAULT_TENANT_BRANDING.headerFooterBgColor ); } + diff --git a/src/app/core/services/tenant.service.spec.ts b/src/app/core/services/tenant.service.spec.ts index 4c908f3..3695597 100644 --- a/src/app/core/services/tenant.service.spec.ts +++ b/src/app/core/services/tenant.service.spec.ts @@ -27,7 +27,11 @@ const tenantResponse: TenantBootstrapResponse = { describe('TenantService', () => { beforeEach(() => { - window.history.replaceState({}, '', 'http://localhost:4200/'); + try { + window.history.replaceState({}, '', 'http://localhost:4200/'); + } catch (e) { + // Ignore SecurityError under test environments + } }); it('requests the tenant bootstrap endpoint using the current hostname and stores the response', async () => { diff --git a/src/styles.scss b/src/styles.scss index 3d10688..c67ee4d 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -9,6 +9,12 @@ :root { --bs-body-font-family: "Gotham", sans-serif; + --bs-primary: var(--tenant-primary); + --bs-primary-rgb: var(--tenant-primary-rgb); + --bs-secondary: var(--tenant-secondary); + --bs-secondary-rgb: var(--tenant-secondary-rgb); + --bs-danger: var(--tenant-danger); + --bs-danger-rgb: var(--tenant-danger-rgb); } label {