feat(ui): adopt persona visibility directives on mounted shells [SPRINT-016]
Apply stellaAuditorOnly and stellaOperatorOnly structural directives on evidence-audit, promotions, and evidence-export surfaces with ViewModeToggle surfaced for persona switching. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { EvidenceAuditOverviewComponent } from './evidence-audit-overview.component';
|
||||
import { ViewModeService } from '../../core/services/view-mode.service';
|
||||
|
||||
describe('EvidenceAuditOverviewComponent (persona visibility)', () => {
|
||||
let fixture: ComponentFixture<EvidenceAuditOverviewComponent>;
|
||||
let service: ViewModeService;
|
||||
|
||||
beforeEach(async () => {
|
||||
localStorage.clear();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [EvidenceAuditOverviewComponent, RouterTestingModule],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(EvidenceAuditOverviewComponent);
|
||||
service = TestBed.inject(ViewModeService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('should render view-mode toggle', () => {
|
||||
service.setMode('operator');
|
||||
fixture.detectChanges();
|
||||
|
||||
const toggle = fixture.nativeElement.querySelector('stella-view-mode-toggle');
|
||||
expect(toggle).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should hide audit events and proof chains in operator mode', () => {
|
||||
service.setMode('operator');
|
||||
fixture.detectChanges();
|
||||
|
||||
const labels: string[] = Array.from(
|
||||
fixture.nativeElement.querySelectorAll('.stat-label')
|
||||
).map((el: any) => el.textContent.trim());
|
||||
|
||||
expect(labels).toContain('Evidence Packs');
|
||||
expect(labels).toContain('Pending Exports');
|
||||
expect(labels).not.toContain('Audit Events Today');
|
||||
expect(labels).not.toContain('Proof Chains');
|
||||
});
|
||||
|
||||
it('should show audit events and proof chains in auditor mode', () => {
|
||||
service.setMode('auditor');
|
||||
fixture.detectChanges();
|
||||
|
||||
const labels: string[] = Array.from(
|
||||
fixture.nativeElement.querySelectorAll('.stat-label')
|
||||
).map((el: any) => el.textContent.trim());
|
||||
|
||||
expect(labels).toContain('Evidence Packs');
|
||||
expect(labels).toContain('Audit Events Today');
|
||||
expect(labels).toContain('Proof Chains');
|
||||
});
|
||||
|
||||
it('should hide pending exports in auditor mode', () => {
|
||||
service.setMode('auditor');
|
||||
fixture.detectChanges();
|
||||
|
||||
const labels: string[] = Array.from(
|
||||
fixture.nativeElement.querySelectorAll('.stat-label')
|
||||
).map((el: any) => el.textContent.trim());
|
||||
|
||||
expect(labels).not.toContain('Pending Exports');
|
||||
});
|
||||
|
||||
it('should react deterministically to mode toggle', () => {
|
||||
service.setMode('operator');
|
||||
fixture.detectChanges();
|
||||
|
||||
let labels: string[] = Array.from(
|
||||
fixture.nativeElement.querySelectorAll('.stat-label')
|
||||
).map((el: any) => el.textContent.trim());
|
||||
expect(labels).not.toContain('Proof Chains');
|
||||
|
||||
service.setMode('auditor');
|
||||
fixture.detectChanges();
|
||||
|
||||
labels = Array.from(
|
||||
fixture.nativeElement.querySelectorAll('.stat-label')
|
||||
).map((el: any) => el.textContent.trim());
|
||||
expect(labels).toContain('Proof Chains');
|
||||
|
||||
service.setMode('operator');
|
||||
fixture.detectChanges();
|
||||
|
||||
labels = Array.from(
|
||||
fixture.nativeElement.querySelectorAll('.stat-label')
|
||||
).map((el: any) => el.textContent.trim());
|
||||
expect(labels).not.toContain('Proof Chains');
|
||||
});
|
||||
});
|
||||
@@ -15,6 +15,9 @@ import {
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { AuditorOnlyDirective } from '../../shared/directives/auditor-only.directive';
|
||||
import { OperatorOnlyDirective } from '../../shared/directives/operator-only.directive';
|
||||
import { ViewModeToggleComponent } from '../../shared/components/view-mode-toggle/view-mode-toggle.component';
|
||||
|
||||
interface EvidenceQuickViewTile {
|
||||
id: string;
|
||||
@@ -29,7 +32,7 @@ type EvidenceHomeMode = 'normal' | 'degraded' | 'empty';
|
||||
@Component({
|
||||
selector: 'app-evidence-audit-overview',
|
||||
standalone: true,
|
||||
imports: [RouterLink],
|
||||
imports: [RouterLink, AuditorOnlyDirective, OperatorOnlyDirective, ViewModeToggleComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div class="evidence-audit-overview">
|
||||
@@ -40,6 +43,7 @@ type EvidenceHomeMode = 'normal' | 'degraded' | 'empty';
|
||||
Retrieve, verify, export, and audit evidence for every release, bundle, environment, and approval decision.
|
||||
</p>
|
||||
</div>
|
||||
<stella-view-mode-toggle />
|
||||
</header>
|
||||
|
||||
<section class="mode-toggle" aria-label="Evidence home state mode">
|
||||
@@ -125,22 +129,22 @@ type EvidenceHomeMode = 'normal' | 'degraded' | 'empty';
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Quick Stats -->
|
||||
<!-- Quick Stats (auditor detail: audit events, proof chains) -->
|
||||
<section class="stats-section" aria-label="Evidence statistics">
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">{{ stats().totalPacks.toLocaleString() }}</span>
|
||||
<span class="stat-label">Evidence Packs</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-item" *stellaAuditorOnly>
|
||||
<span class="stat-value">{{ stats().auditEventsToday.toLocaleString() }}</span>
|
||||
<span class="stat-label">Audit Events Today</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-item" *stellaAuditorOnly>
|
||||
<span class="stat-value">{{ stats().proofChains.toLocaleString() }}</span>
|
||||
<span class="stat-label">Proof Chains</span>
|
||||
</div>
|
||||
<div class="stat-item" [class.warning]="stats().pendingExports > 0">
|
||||
<div class="stat-item" *stellaOperatorOnly [class.warning]="stats().pendingExports > 0">
|
||||
<span class="stat-value">{{ stats().pendingExports }}</span>
|
||||
<span class="stat-label">Pending Exports</span>
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
StellaBundleExportResult,
|
||||
} from './evidence-export.models';
|
||||
import { StellaBundleExportButtonComponent } from './stella-bundle-export-button/stella-bundle-export-button.component';
|
||||
import { OperatorOnlyDirective } from '../../shared/directives/operator-only.directive';
|
||||
import { ViewModeToggleComponent } from '../../shared/components/view-mode-toggle/view-mode-toggle.component';
|
||||
|
||||
/**
|
||||
* Export Center Component (Sprint: SPRINT_20251229_016)
|
||||
@@ -25,7 +27,7 @@ import { StellaBundleExportButtonComponent } from './stella-bundle-export-button
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-export-center',
|
||||
imports: [FormsModule, StellaBundleExportButtonComponent],
|
||||
imports: [FormsModule, StellaBundleExportButtonComponent, OperatorOnlyDirective, ViewModeToggleComponent],
|
||||
template: `
|
||||
<div class="export-center">
|
||||
<header class="page-header">
|
||||
@@ -34,6 +36,7 @@ import { StellaBundleExportButtonComponent } from './stella-bundle-export-button
|
||||
<h1>Export Center</h1>
|
||||
<p>Configure export profiles and monitor export runs.</p>
|
||||
</div>
|
||||
<stella-view-mode-toggle />
|
||||
<!-- Quick Actions (SB-003) -->
|
||||
<div class="quick-actions">
|
||||
<app-stella-bundle-export-button
|
||||
@@ -150,7 +153,7 @@ import { StellaBundleExportButtonComponent } from './stella-bundle-export-button
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="profile-actions">
|
||||
<div class="profile-actions" *stellaOperatorOnly>
|
||||
<button class="btn btn-sm btn-primary" (click)="runProfile(profile)">
|
||||
Run Now
|
||||
</button>
|
||||
|
||||
@@ -13,6 +13,9 @@ import { catchError, of } from 'rxjs';
|
||||
|
||||
import { APPROVAL_API } from '../../core/api/approval.client';
|
||||
import type { ApprovalDetail, GateStatus } from '../../core/api/approval.models';
|
||||
import { AuditorOnlyDirective } from '../../shared/directives/auditor-only.directive';
|
||||
import { OperatorOnlyDirective } from '../../shared/directives/operator-only.directive';
|
||||
import { ViewModeToggleComponent } from '../../shared/components/view-mode-toggle/view-mode-toggle.component';
|
||||
|
||||
type DetailTab =
|
||||
| 'overview'
|
||||
@@ -27,7 +30,7 @@ type DetailTab =
|
||||
@Component({
|
||||
selector: 'app-promotion-detail',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, RouterLink],
|
||||
imports: [CommonModule, FormsModule, RouterLink, AuditorOnlyDirective, OperatorOnlyDirective, ViewModeToggleComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div class="promotion-detail">
|
||||
@@ -54,9 +57,12 @@ type DetailTab =
|
||||
| requested by {{ promotion()!.requestedBy }}
|
||||
</p>
|
||||
</div>
|
||||
<span class="status-badge status-badge--{{ promotion()!.status }}">
|
||||
{{ promotion()!.status }}
|
||||
</span>
|
||||
<div class="promotion-detail__header-actions">
|
||||
<stella-view-mode-toggle />
|
||||
<span class="status-badge status-badge--{{ promotion()!.status }}">
|
||||
{{ promotion()!.status }}
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="promotion-detail__identity" aria-label="Bundle version identity">
|
||||
@@ -104,7 +110,7 @@ type DetailTab =
|
||||
</div>
|
||||
|
||||
@if (promotion()!.status === 'pending') {
|
||||
<div class="decision-box">
|
||||
<div class="decision-box" *stellaOperatorOnly>
|
||||
<label for="decisionComment">Decision comment</label>
|
||||
<textarea
|
||||
id="decisionComment"
|
||||
@@ -197,7 +203,7 @@ type DetailTab =
|
||||
</section>
|
||||
}
|
||||
@case ('evidence') {
|
||||
<section class="panel" aria-label="Evidence snapshot">
|
||||
<section class="panel" aria-label="Evidence snapshot" *stellaAuditorOnly>
|
||||
<h2>Evidence Used for Decision</h2>
|
||||
<p>
|
||||
Evidence packet identifiers are not provided in this contract; use canonical Evidence and Audit surfaces for promotion-linked retrieval.
|
||||
@@ -206,7 +212,7 @@ type DetailTab =
|
||||
</section>
|
||||
}
|
||||
@case ('replay') {
|
||||
<section class="panel" aria-label="Replay and verify">
|
||||
<section class="panel" aria-label="Replay and verify" *stellaAuditorOnly>
|
||||
<h2>Replay / Verify Decision</h2>
|
||||
<p>Replay and verification are delegated to Evidence and Audit.</p>
|
||||
<a routerLink="/evidence/verify-replay" class="link-sm">Open replay and verify -></a>
|
||||
@@ -262,6 +268,12 @@ type DetailTab =
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.promotion-detail__header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.promotion-detail__title {
|
||||
font-size: 1.375rem;
|
||||
font-weight: 600;
|
||||
|
||||
Reference in New Issue
Block a user