feat: dead-config sweep, test suite foundation, widget settingsSchema validation
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Sprint G: audited every BootstrapConfig field for a real runtime consumer (docs/DEAD-CONFIG-AUDIT.md). Wired 3 previously-dead editable fields: footer.logoUrl, company.address.street/contacts.phone, catalog.suggestionsEnabled. Remaining dead fields needing a business/design decision tracked in PRODUCT_BACKLOG.md/KNOWN-ISSUES.md, not silently left. Sprint H: 6 new spec files (test count 57 -> 83), covering ProjectEditorFacade (undo/redo, draft persistence, publish gating), AdminAnalyticsFacade (never-fabricate-a-number contract), and regression coverage for this session's carousel/hero/profile-toggle fixes. Sprint I: widget settingsSchema (declared in widget-manifest.json, never validated) now enforced via a new lightweight schema check in ProjectValidator, surfaced through the existing issuesByField pipeline. Same check reused in diagnostics so editor and diagnostics can't disagree. Verification: tsc clean, ng build clean, 83/83 tests pass, barry-cache validate clean (2 pre-existing unrelated warnings only). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
<div class="app-footer__container page-container">
|
||||
<div class="app-footer__top">
|
||||
<div class="app-footer__brand">
|
||||
<app-logo />
|
||||
<app-logo [srcOverride]="footerLogoUrl()" />
|
||||
<p>{{ 'footer.description' | translate }}</p>
|
||||
</div>
|
||||
|
||||
@@ -46,6 +46,12 @@
|
||||
@if (contactEmail) {
|
||||
<a [href]="'mailto:' + contactEmail">{{ contactEmail }}</a>
|
||||
}
|
||||
@if (contactPhone) {
|
||||
<a [href]="'tel:' + contactPhone">{{ contactPhone }}</a>
|
||||
}
|
||||
@if (companyAddress) {
|
||||
<span class="app-footer__address">{{ companyAddress }}</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -21,6 +21,7 @@ export class FooterComponent {
|
||||
readonly footerGroups = signal<FooterResolvedGroup[]>([]);
|
||||
readonly paymentIcons = signal<FooterPaymentIcon[]>([]);
|
||||
readonly copyrightText = signal('');
|
||||
readonly footerLogoUrl = signal<string | undefined>(undefined);
|
||||
|
||||
private readonly configService = inject(ConfigService);
|
||||
|
||||
@@ -35,6 +36,7 @@ export class FooterComponent {
|
||||
this.footerGroups.set([]);
|
||||
this.paymentIcons.set([]);
|
||||
this.copyrightText.set('');
|
||||
this.footerLogoUrl.set(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -42,6 +44,7 @@ export class FooterComponent {
|
||||
this.footerGroups.set(model.groups);
|
||||
this.paymentIcons.set(model.paymentIcons);
|
||||
this.copyrightText.set(model.copyrightText);
|
||||
this.footerLogoUrl.set(model.logoUrl);
|
||||
});
|
||||
|
||||
this.configService.loadBootstrap().subscribe();
|
||||
@@ -56,4 +59,12 @@ export class FooterComponent {
|
||||
get contactEmail(): string {
|
||||
return this.uiRuntime.contactEmail();
|
||||
}
|
||||
|
||||
get contactPhone(): string {
|
||||
return this.uiRuntime.contactPhone();
|
||||
}
|
||||
|
||||
get companyAddress(): string {
|
||||
return this.uiRuntime.companyAddress();
|
||||
}
|
||||
}
|
||||
|
||||
86
src/app/components/header/header.component.spec.ts
Normal file
86
src/app/components/header/header.component.spec.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { of } from 'rxjs';
|
||||
import { BootstrapConfig } from '../../shared/models/config';
|
||||
import { CONFIG_PROVIDER } from '../../core/config/config-provider.token';
|
||||
import { ConfigService } from '../../core/config/config.service';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
import { HeaderComponent } from './header.component';
|
||||
|
||||
function makeBootstrap(): BootstrapConfig {
|
||||
return {
|
||||
schemaVersion: '1', generatedAt: new Date().toISOString(),
|
||||
tenant: { id: 't1', slug: 't1', code: 't1', host: 'dexar.market', name: 'Dexar', websiteBaseUrl: 'https://dexar.market', builderBaseUrl: 'https://dexar.market', backofficeBaseUrl: 'https://dexar.market', defaultLocale: 'en', supportedLocales: ['en'], defaultCurrency: 'USD', supportedCurrencies: ['USD'], timezone: 'UTC' },
|
||||
branding: { brandName: 'Dexar', legalName: 'Dexar LLC', logoUrl: 'logo.png', faviconUrl: 'favicon.png' },
|
||||
theme: { themeId: 'default', mode: 'light', palette: {} as any, typography: {} as any, spacing: {} as any, borderRadiusScale: {}, shadows: {}, iconSet: 'default' },
|
||||
company: { companyName: 'Dexar LLC', address: { country: 'US', city: 'NY' }, contacts: { email: 'sales@dexar.market' } },
|
||||
featureFlags: {} as any,
|
||||
apiEndpoints: {} as any,
|
||||
localization: { defaultLocale: 'en', supportedLocales: ['en'], currencyByLocale: {}, dictionaries: [] },
|
||||
seo: { default: { title: 'Dexar', description: 'Dexar' }, byPageKey: {} },
|
||||
permissions: { definitions: [], roles: [] },
|
||||
header: { showLogo: true, showSearch: true, showCategories: true, showLanguages: true, showCart: true, showProfile: true, showWishlist: true, showCompare: true, showRegion: true, sticky: true, layout: 'default' },
|
||||
navigation: { header: [], footer: [] },
|
||||
pages: [],
|
||||
} as unknown as BootstrapConfig;
|
||||
}
|
||||
|
||||
describe('HeaderComponent profile control (login/logout gating regression)', () => {
|
||||
function configure(isAuthenticated: boolean): void {
|
||||
// Full fake - TelegramLoginComponent (rendered inside the profile control) reads
|
||||
// several signals/methods off AuthService directly, not just isAuthenticated.
|
||||
const fakeAuth = {
|
||||
session: () => null,
|
||||
status: () => (isAuthenticated ? 'authenticated' : 'unauthenticated'),
|
||||
isAuthenticated: () => isAuthenticated,
|
||||
showLoginDialog: () => false,
|
||||
displayName: () => null,
|
||||
requestLogin: jasmine.createSpy('requestLogin'),
|
||||
logout: jasmine.createSpy('logout'),
|
||||
hideLogin: jasmine.createSpy('hideLogin'),
|
||||
createWebSession: () => of({ webSessionID: 'x', botLoginUrl: '' }),
|
||||
checkSessionOnce: () => of(null),
|
||||
getTelegramAppLoginUrl: () => '',
|
||||
onTelegramLoginComplete: jasmine.createSpy('onTelegramLoginComplete'),
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
provideRouter([]),
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
{ provide: CONFIG_PROVIDER, useValue: { loadBootstrap: () => of(makeBootstrap()) } },
|
||||
{ provide: AuthService, useValue: fakeAuth },
|
||||
],
|
||||
});
|
||||
|
||||
// Deterministically prime the bootstrap snapshot before component creation -
|
||||
// resolveHeaderConfig() reads getBootstrapSnapshot() synchronously and falls
|
||||
// back to DEFAULT_HEADER_CONFIG (showProfile: false) if it isn't populated yet.
|
||||
TestBed.inject(ConfigService).loadBootstrap().subscribe();
|
||||
}
|
||||
|
||||
it('shows a login button (not logout) when logged out', () => {
|
||||
configure(false);
|
||||
const fixture = TestBed.createComponent(HeaderComponent);
|
||||
fixture.detectChanges();
|
||||
|
||||
// Aria-labels render translated text (Russian by default), so assert on the
|
||||
// icon name attribute instead - stable regardless of active language.
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('app-icon[name="user"]')).toBeTruthy();
|
||||
expect(compiled.querySelector('app-icon[name="logOut"]')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('shows a logout button (not login) when logged in - never both', () => {
|
||||
configure(true);
|
||||
const fixture = TestBed.createComponent(HeaderComponent);
|
||||
fixture.detectChanges();
|
||||
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('app-icon[name="logOut"]')).toBeTruthy();
|
||||
expect(compiled.querySelector('app-icon[name="user"]')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
|
||||
import { UiRuntimeFacade } from '../../facades/runtime/ui-runtime.facade';
|
||||
|
||||
@Component({
|
||||
@@ -17,6 +17,9 @@ import { UiRuntimeFacade } from '../../facades/runtime/ui-runtime.facade';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class LogoComponent {
|
||||
/** Overrides the default (branding.logoUrl) logo, e.g. footer.logoUrl. */
|
||||
@Input() srcOverride?: string;
|
||||
|
||||
constructor(private readonly uiRuntime: UiRuntimeFacade) {}
|
||||
|
||||
get brandName(): string {
|
||||
@@ -24,6 +27,6 @@ export class LogoComponent {
|
||||
}
|
||||
|
||||
get logoPath(): string {
|
||||
return this.uiRuntime.logoUrl();
|
||||
return this.srcOverride || this.uiRuntime.logoUrl();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user