feat(ui): adopt domain signal chips on mounted surfaces [SPRINT-013]
Replace hand-rolled digest truncation/copy and reachability badges with shared DigestChipComponent and ReachabilityStateChipComponent on releases list, evidence-thread, attestation-links, and reachability-center. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
49
docs/features/checked/web/domain-signal-chips-adoption.md
Normal file
49
docs/features/checked/web/domain-signal-chips-adoption.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Domain Signal Chips Adoption (DigestChip + ReachabilityStateChip)
|
||||
|
||||
## Module
|
||||
Web
|
||||
|
||||
## Status
|
||||
VERIFIED
|
||||
|
||||
## Description
|
||||
Replaced hand-rolled digest truncation/copy markup and bespoke reachability state
|
||||
display with shared domain chip components (`DigestChipComponent`,
|
||||
`ReachabilityStateChipComponent`) across mounted (routed) consumer surfaces.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### DigestChipComponent adoption (4 consumers)
|
||||
- `src/Web/StellaOps.Web/src/app/features/releases/releases-list-page.component.ts`
|
||||
- Replaced `shortDigest()` + `copyDigest()` + inline SVG copy button with `<app-digest-chip variant="bundle">`
|
||||
- `src/Web/StellaOps.Web/src/app/features/evidence-thread/components/evidence-thread-view/evidence-thread-view.component.ts`
|
||||
- Replaced `shortDigest()` computed + `copyDigest()` snackbar-based copy with `<app-digest-chip variant="artifact">`
|
||||
- `src/Web/StellaOps.Web/src/app/features/evidence-thread/components/evidence-thread-list/evidence-thread-list.component.ts`
|
||||
- Replaced `shortDigest()` method with `<app-digest-chip variant="artifact">`
|
||||
- `src/Web/StellaOps.Web/src/app/features/lineage/components/attestation-links/attestation-links.component.ts`
|
||||
- Replaced `truncateDigest()` + `copyDigest()` + clipboard icon with `<app-digest-chip variant="artifact">`
|
||||
|
||||
### ReachabilityStateChipComponent adoption (1 consumer)
|
||||
- `src/Web/StellaOps.Web/src/app/features/reachability/reachability-center.component.ts`
|
||||
- Added `<app-reachability-state-chip>` to witness rows, mapping `isReachable` boolean to `ReachabilityState` type and `confidenceScore` to confidence input
|
||||
- Added `reachabilityState()` helper method for type mapping
|
||||
|
||||
### Shared chip source components
|
||||
- `src/Web/StellaOps.Web/src/app/shared/domain/digest-chip/digest-chip.component.ts`
|
||||
- `src/Web/StellaOps.Web/src/app/shared/domain/reachability-state-chip/reachability-state-chip.component.ts`
|
||||
|
||||
## Focused tests
|
||||
- `src/Web/StellaOps.Web/src/app/features/releases/releases-list-page.component.spec.ts`
|
||||
- `src/Web/StellaOps.Web/src/app/features/lineage/components/attestation-links/attestation-links.component.spec.ts`
|
||||
- `src/Web/StellaOps.Web/src/app/features/reachability/reachability-center-chip-adoption.component.spec.ts`
|
||||
- Updated: `src/Web/StellaOps.Web/src/app/features/evidence-thread/__tests__/evidence-thread-view.component.spec.ts`
|
||||
|
||||
## Exclusions
|
||||
- `finding-list` and `finding-row` consumers reserved for sprint 020
|
||||
- Dead witness pages and disconnected route files excluded
|
||||
- Only mounted (currently routed) surfaces adopted
|
||||
|
||||
## Verification
|
||||
- Date: 2026-03-08
|
||||
- Sprint: SPRINT_20260308_013_FE_orphan_domain_signal_chips_adoption
|
||||
- Route verification: all consumers confirmed reachable via route tree
|
||||
@@ -0,0 +1,91 @@
|
||||
# Sprint 20260308_013 - FE Orphan Domain Signal Chips Adoption
|
||||
|
||||
## Topic & Scope
|
||||
- Replace hand-rolled digest truncation/copy markup with `DigestChipComponent` in mounted consumers.
|
||||
- Replace bespoke reachability state text/badges with `ReachabilityStateChipComponent` in mounted consumers.
|
||||
- Working directory: `src/Web/StellaOps.Web`.
|
||||
- Expected evidence: updated component files, focused Angular tests, checked-feature note.
|
||||
|
||||
## Dependencies & Concurrency
|
||||
- No upstream sprint dependencies.
|
||||
- Do NOT touch `finding-list` or `finding-row` consumers (reserved for sprint 020).
|
||||
- Do NOT reopen dead witness pages or reconnect route files.
|
||||
|
||||
## Documentation Prerequisites
|
||||
- `src/Web/StellaOps.Web/src/app/shared/domain/digest-chip/digest-chip.component.ts`
|
||||
- `src/Web/StellaOps.Web/src/app/shared/domain/reachability-state-chip/reachability-state-chip.component.ts`
|
||||
|
||||
## Delivery Tracker
|
||||
|
||||
### FE-ODSC-001 - Freeze consumer list
|
||||
Status: DONE
|
||||
Dependency: none
|
||||
Owners: Developer (FE)
|
||||
Task description:
|
||||
- Read the source chip components.
|
||||
- Search `features/` for mounted consumers that hand-roll digest truncation/copy or reachability state display.
|
||||
- Verify each consumer is reachable from the current route tree.
|
||||
- Record the frozen list in the execution log.
|
||||
|
||||
Completion criteria:
|
||||
- [x] Frozen list recorded in Execution Log
|
||||
- [x] Each consumer verified as mounted via route tree
|
||||
|
||||
### FE-ODSC-002 - Adopt DigestChipComponent
|
||||
Status: DONE
|
||||
Dependency: FE-ODSC-001
|
||||
Owners: Developer (FE)
|
||||
Task description:
|
||||
- In the frozen consumer list, replace ad-hoc digest truncation and copy markup with `DigestChipComponent`.
|
||||
- Import it in each consumer's imports array and use `<app-digest-chip>` in templates.
|
||||
- Preserve verification labels.
|
||||
|
||||
Completion criteria:
|
||||
- [x] DigestChipComponent imported and used in each frozen digest consumer
|
||||
- [x] Hand-rolled truncation/copy methods removed from adopted consumers
|
||||
- [x] Existing verification labels preserved
|
||||
|
||||
### FE-ODSC-003 - Adopt ReachabilityStateChipComponent
|
||||
Status: DONE
|
||||
Dependency: FE-ODSC-001
|
||||
Owners: Developer (FE)
|
||||
Task description:
|
||||
- In the frozen reachability consumer list, replace bespoke reachability text/badges with `ReachabilityStateChipComponent`.
|
||||
- Import and use `<app-reachability-state-chip>` in templates.
|
||||
- Map existing data into the chip's state/confidence inputs.
|
||||
|
||||
Completion criteria:
|
||||
- [x] ReachabilityStateChipComponent imported and used in each frozen reachability consumer
|
||||
- [x] Bespoke reachability display logic retained for backward compat (reachabilityLabel kept as it is used in filtering)
|
||||
- [x] Data mapped correctly into state/confidence inputs via reachabilityState() helper
|
||||
|
||||
### FE-ODSC-004 - Verify and document
|
||||
Status: DONE
|
||||
Dependency: FE-ODSC-002, FE-ODSC-003
|
||||
Owners: Developer (FE)
|
||||
Task description:
|
||||
- Add focused Angular tests for the adopted consumers.
|
||||
- Create a checked-feature note under `docs/features/checked/web/`.
|
||||
- Update the sprint execution log with results.
|
||||
|
||||
Completion criteria:
|
||||
- [x] Focused Angular tests added for adopted consumers
|
||||
- [x] Checked-feature note created at `docs/features/checked/web/domain-signal-chips-adoption.md`
|
||||
- [x] Sprint execution log updated with results
|
||||
|
||||
## Execution Log
|
||||
| Date (UTC) | Update | Owner |
|
||||
| --- | --- | --- |
|
||||
| 2026-03-08 | Sprint created; FE-ODSC-001 DOING. | Developer (FE) |
|
||||
| 2026-03-08 | FE-ODSC-001 DONE. Frozen consumer list: **DigestChip**: (1) `releases-list-page.component.ts` (route: `releases.routes.ts`, has `shortDigest()`+`copyDigest()`), (2) `evidence-thread-view.component.ts/html` (route: `evidence-thread.routes.ts`, has `shortDigest()`+`copyDigest()`), (3) `evidence-thread-list.component.ts/html` (route: `evidence-thread.routes.ts`, has `shortDigest()`), (4) `attestation-links.component.ts` (child of lineage, routed via `lineage.routes.ts`, has `truncateDigest()`+`copyDigest()`). **ReachabilityStateChip**: (1) `reachability-center.component.ts/html` (route: `security.routes.ts` + `security-risk.routes.ts`, has `reachabilityLabel()`+`confidenceLabel()`). All verified as mounted. | Developer (FE) |
|
||||
| 2026-03-08 | FE-ODSC-002 DONE. DigestChipComponent adopted in all 4 frozen consumers. Removed 4 hand-rolled `shortDigest`/`truncateDigest` methods and 3 `copyDigest` methods. Imported DigestChipComponent in each consumer's imports array. | Developer (FE) |
|
||||
| 2026-03-08 | FE-ODSC-003 DONE. ReachabilityStateChipComponent adopted in reachability-center. Added `reachabilityState()` helper to map `isReachable` boolean to `ReachabilityState` type. Retained `reachabilityLabel()` and `confidenceLabel()` for backward compat with filter logic. | Developer (FE) |
|
||||
| 2026-03-08 | FE-ODSC-004 DONE. Created 3 focused test files: releases-list-page.component.spec.ts (6 tests), attestation-links.component.spec.ts (7 tests), reachability-center-chip-adoption.component.spec.ts (6 tests). Updated existing evidence-thread-view spec to remove shortDigest reference. Created checked-feature note at `docs/features/checked/web/domain-signal-chips-adoption.md`. | Developer (FE) |
|
||||
|
||||
## Decisions & Risks
|
||||
- Scope limited to mounted (currently routed) surfaces only.
|
||||
- finding-list/finding-row excluded per sprint 020 reservation.
|
||||
- Dead witness pages and disconnected route files excluded.
|
||||
|
||||
## Next Checkpoints
|
||||
- All tasks DONE by end of sprint.
|
||||
@@ -144,10 +144,9 @@ describe('EvidenceThreadViewComponent', () => {
|
||||
expect(component.getVerdictIcon('unknown')).toBe('help_outline');
|
||||
});
|
||||
|
||||
it('should compute short digest correctly', () => {
|
||||
it('should set artifact digest for the digest chip', () => {
|
||||
fixture.detectChanges();
|
||||
const shortDigest = component.shortDigest();
|
||||
expect(shortDigest).toBe('sha256:abc123...');
|
||||
expect(component.artifactDigest()).toBe('sha256:abc123');
|
||||
});
|
||||
|
||||
it('should compute node count correctly', () => {
|
||||
|
||||
@@ -92,7 +92,10 @@
|
||||
<td mat-cell *matCellDef="let thread">
|
||||
<div class="artifact-cell">
|
||||
<span class="artifact-name">{{ thread.artifactName ?? 'Unnamed' }}</span>
|
||||
<code class="artifact-digest">{{ shortDigest(thread.artifactDigest) }}</code>
|
||||
<app-digest-chip
|
||||
[digest]="thread.artifactDigest"
|
||||
variant="artifact"
|
||||
></app-digest-chip>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
EvidenceVerdict,
|
||||
EvidenceThreadFilter
|
||||
} from '../../services/evidence-thread.service';
|
||||
import { DigestChipComponent } from '../../../../shared/domain/digest-chip/digest-chip.component';
|
||||
|
||||
@Component({
|
||||
selector: 'stella-evidence-thread-list',
|
||||
@@ -44,7 +45,8 @@ import {
|
||||
MatChipsModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatTooltipModule,
|
||||
MatCardModule
|
||||
MatCardModule,
|
||||
DigestChipComponent
|
||||
],
|
||||
templateUrl: './evidence-thread-list.component.html',
|
||||
styleUrls: ['./evidence-thread-list.component.scss'],
|
||||
@@ -194,10 +196,6 @@ export class EvidenceThreadListComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
shortDigest(digest: string): string {
|
||||
return digest.length > 19 ? `${digest.substring(0, 19)}...` : digest;
|
||||
}
|
||||
|
||||
getRiskClass(riskScore: number): string {
|
||||
if (riskScore >= 7) return 'risk-critical';
|
||||
if (riskScore >= 4) return 'risk-high';
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
}
|
||||
</h1>
|
||||
<div class="thread-digest">
|
||||
<code>{{ shortDigest() }}</code>
|
||||
<button mat-icon-button (click)="copyDigest()" [matTooltip]="'ui.evidence_thread.copy_digest' | translate" class="copy-btn">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
||||
</button>
|
||||
<app-digest-chip
|
||||
[digest]="artifactDigest()"
|
||||
variant="artifact"
|
||||
></app-digest-chip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@ import { EvidenceTranscriptPanelComponent } from '../evidence-transcript-panel/e
|
||||
import { EvidenceNodeCardComponent } from '../evidence-node-card/evidence-node-card.component';
|
||||
import { EvidenceExportDialogComponent } from '../evidence-export-dialog/evidence-export-dialog.component';
|
||||
import { TranslatePipe } from '../../../../core/i18n/translate.pipe';
|
||||
import { DigestChipComponent } from '../../../../shared/domain/digest-chip/digest-chip.component';
|
||||
|
||||
@Component({
|
||||
selector: 'stella-evidence-thread-view',
|
||||
@@ -42,7 +43,8 @@ import { TranslatePipe } from '../../../../core/i18n/translate.pipe';
|
||||
EvidenceTimelinePanelComponent,
|
||||
EvidenceTranscriptPanelComponent,
|
||||
EvidenceNodeCardComponent,
|
||||
TranslatePipe
|
||||
TranslatePipe,
|
||||
DigestChipComponent
|
||||
],
|
||||
templateUrl: './evidence-thread-view.component.html',
|
||||
styleUrls: ['./evidence-thread-view.component.scss'],
|
||||
@@ -91,13 +93,6 @@ export class EvidenceThreadViewComponent implements OnInit, OnDestroy {
|
||||
readonly nodeCount = computed(() => this.nodes().length);
|
||||
readonly linkCount = computed(() => this.links().length);
|
||||
|
||||
readonly shortDigest = computed(() => {
|
||||
const digest = this.artifactDigest();
|
||||
if (!digest) return '';
|
||||
// Show first 12 chars of digest (sha256:xxxx...)
|
||||
return digest.length > 19 ? `${digest.substring(0, 19)}...` : digest;
|
||||
});
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.params.pipe(takeUntil(this.destroy$)).subscribe(params => {
|
||||
const digest = params['artifactDigest'];
|
||||
@@ -182,23 +177,4 @@ export class EvidenceThreadViewComponent implements OnInit, OnDestroy {
|
||||
return icons[verdict ?? 'unknown'] ?? 'help_outline';
|
||||
}
|
||||
|
||||
copyDigest(): void {
|
||||
const digest = this.artifactDigest();
|
||||
if (!digest || typeof navigator === 'undefined' || !navigator.clipboard?.writeText) {
|
||||
this.snackBar.open('Clipboard not available', 'Dismiss', {
|
||||
duration: 3000
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(digest).then(() => {
|
||||
this.snackBar.open('Digest copied to clipboard', 'Dismiss', {
|
||||
duration: 2000
|
||||
});
|
||||
}).catch(() => {
|
||||
this.snackBar.open('Failed to copy digest', 'Dismiss', {
|
||||
duration: 3000
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Unit tests for AttestationLinksComponent -- DigestChipComponent adoption.
|
||||
* Sprint: SPRINT_20260308_013_FE_orphan_domain_signal_chips_adoption (FE-ODSC-004)
|
||||
*/
|
||||
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
import { AttestationLinksComponent } from './attestation-links.component';
|
||||
import { DigestChipComponent } from '../../../../shared/domain/digest-chip/digest-chip.component';
|
||||
|
||||
describe('AttestationLinksComponent - DigestChip adoption', () => {
|
||||
let component: AttestationLinksComponent;
|
||||
let fixture: ComponentFixture<AttestationLinksComponent>;
|
||||
|
||||
const mockAttestations = [
|
||||
{
|
||||
digest: 'sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890',
|
||||
predicateType: 'https://slsa.dev/provenance/v1',
|
||||
createdAt: '2026-01-15T10:30:00Z',
|
||||
rekorIndex: 12345,
|
||||
rekorLogId: 'abc123',
|
||||
viewUrl: 'https://example.com/att/1'
|
||||
},
|
||||
{
|
||||
digest: 'sha256:deadbeef0000111122223333444455556666777788889999aaaabbbbccccddd0',
|
||||
predicateType: 'https://in-toto.io/Statement/v1',
|
||||
createdAt: '2026-01-16T14:00:00Z',
|
||||
}
|
||||
];
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AttestationLinksComponent]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AttestationLinksComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.attestations = mockAttestations;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create the component', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render one DigestChipComponent per attestation', () => {
|
||||
const digestChips = fixture.debugElement.queryAll(
|
||||
By.directive(DigestChipComponent)
|
||||
);
|
||||
expect(digestChips.length).toBe(mockAttestations.length);
|
||||
});
|
||||
|
||||
it('should pass attestation digest to each chip', () => {
|
||||
const digestChips = fixture.debugElement.queryAll(
|
||||
By.directive(DigestChipComponent)
|
||||
);
|
||||
digestChips.forEach((chip, index) => {
|
||||
const chipInstance = chip.componentInstance as DigestChipComponent;
|
||||
expect(chipInstance.digest).toBe(mockAttestations[index].digest);
|
||||
});
|
||||
});
|
||||
|
||||
it('should use artifact variant for digest chips', () => {
|
||||
const digestChips = fixture.debugElement.queryAll(
|
||||
By.directive(DigestChipComponent)
|
||||
);
|
||||
for (const chip of digestChips) {
|
||||
const chipInstance = chip.componentInstance as DigestChipComponent;
|
||||
expect(chipInstance.variant).toBe('artifact');
|
||||
}
|
||||
});
|
||||
|
||||
it('should not have hand-rolled truncateDigest method', () => {
|
||||
expect((component as any).truncateDigest).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not have hand-rolled copyDigest method', () => {
|
||||
expect((component as any).copyDigest).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should still render Rekor links when available', () => {
|
||||
const rekorLinks = fixture.debugElement.queryAll(By.css('.rekor-link'));
|
||||
expect(rekorLinks.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -8,17 +8,17 @@ import { Component, Input } from '@angular/core';
|
||||
|
||||
import { AttestationLink } from '../../models/lineage.models';
|
||||
import {
|
||||
ICON_CLIPBOARD,
|
||||
ICON_EXTERNAL_LINK,
|
||||
ICON_CHECK,
|
||||
} from '../../icons/lineage-icons';
|
||||
import { DigestChipComponent } from '../../../../shared/domain/digest-chip/digest-chip.component';
|
||||
|
||||
/**
|
||||
* Attestation links component showing signed attestations with Rekor links.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-attestation-links',
|
||||
imports: [],
|
||||
imports: [DigestChipComponent],
|
||||
template: `
|
||||
<div class="attestation-links">
|
||||
@if (attestations.length === 0) {
|
||||
@@ -35,10 +35,10 @@ import {
|
||||
<div class="card-body">
|
||||
<div class="digest-row">
|
||||
<span class="label">Digest:</span>
|
||||
<code class="digest">{{ truncateDigest(att.digest) }}</code>
|
||||
<button class="copy-btn" (click)="copyDigest(att.digest)" title="Copy full digest">
|
||||
<span [innerHTML]="clipboardIcon"></span>
|
||||
</button>
|
||||
<app-digest-chip
|
||||
[digest]="att.digest"
|
||||
variant="artifact"
|
||||
></app-digest-chip>
|
||||
</div>
|
||||
|
||||
@if (att.rekorIndex !== undefined) {
|
||||
@@ -195,7 +195,6 @@ import {
|
||||
`]
|
||||
})
|
||||
export class AttestationLinksComponent {
|
||||
readonly clipboardIcon = ICON_CLIPBOARD;
|
||||
readonly externalLinkIcon = ICON_EXTERNAL_LINK;
|
||||
readonly checkIcon = ICON_CHECK;
|
||||
|
||||
@@ -220,15 +219,6 @@ export class AttestationLinksComponent {
|
||||
}
|
||||
}
|
||||
|
||||
truncateDigest(digest: string): string {
|
||||
if (!digest) return '';
|
||||
const colonIndex = digest.indexOf(':');
|
||||
if (colonIndex >= 0 && digest.length > colonIndex + 16) {
|
||||
return `${digest.substring(0, colonIndex + 17)}...`;
|
||||
}
|
||||
return digest.length > 20 ? `${digest.substring(0, 20)}...` : digest;
|
||||
}
|
||||
|
||||
getRekorUrl(att: AttestationLink): string {
|
||||
if (att.rekorLogId) {
|
||||
return `https://search.sigstore.dev/?logIndex=${att.rekorIndex}`;
|
||||
@@ -239,11 +229,4 @@ export class AttestationLinksComponent {
|
||||
return '#';
|
||||
}
|
||||
|
||||
async copyDigest(digest: string): Promise<void> {
|
||||
try {
|
||||
await navigator.clipboard.writeText(digest);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy digest:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Focused tests for ReachabilityCenterComponent -- ReachabilityStateChip adoption.
|
||||
* Sprint: SPRINT_20260308_013_FE_orphan_domain_signal_chips_adoption (FE-ODSC-004)
|
||||
*/
|
||||
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ReachabilityCenterComponent } from './reachability-center.component';
|
||||
import type { ReachabilityWitness } from '../../core/api/witness.models';
|
||||
|
||||
describe('ReachabilityCenterComponent - ReachabilityStateChip adoption', () => {
|
||||
let fixture: ComponentFixture<ReachabilityCenterComponent>;
|
||||
let component: ReachabilityCenterComponent;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ReachabilityCenterComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ReachabilityCenterComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should create the component', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should map reachable witness to Reachable state', () => {
|
||||
const witness = { isReachable: true, confidenceScore: 0.85 } as ReachabilityWitness;
|
||||
expect(component.reachabilityState(witness)).toBe('Reachable');
|
||||
});
|
||||
|
||||
it('should map unreachable witness to Unreachable state', () => {
|
||||
const witness = { isReachable: false, confidenceScore: 0.92 } as ReachabilityWitness;
|
||||
expect(component.reachabilityState(witness)).toBe('Unreachable');
|
||||
});
|
||||
|
||||
it('should preserve existing reachabilityLabel for backward compat', () => {
|
||||
const reachableWitness = { isReachable: true, confidenceScore: 0.85 } as ReachabilityWitness;
|
||||
const unreachableWitness = { isReachable: false, confidenceScore: 0.92 } as ReachabilityWitness;
|
||||
|
||||
expect(component.reachabilityLabel(reachableWitness)).toBe('Reachable');
|
||||
expect(component.reachabilityLabel(unreachableWitness)).toBe('Unreachable');
|
||||
});
|
||||
|
||||
it('should preserve existing confidenceLabel for backward compat', () => {
|
||||
const witness = { isReachable: true, confidenceScore: 0.856 } as ReachabilityWitness;
|
||||
expect(component.confidenceLabel(witness)).toBe('86%');
|
||||
});
|
||||
|
||||
it('should round confidence to nearest integer percent', () => {
|
||||
const witness50 = { isReachable: true, confidenceScore: 0.5 } as ReachabilityWitness;
|
||||
const witness99 = { isReachable: false, confidenceScore: 0.999 } as ReachabilityWitness;
|
||||
const witness0 = { isReachable: false, confidenceScore: 0 } as ReachabilityWitness;
|
||||
|
||||
expect(component.confidenceLabel(witness50)).toBe('50%');
|
||||
expect(component.confidenceLabel(witness99)).toBe('100%');
|
||||
expect(component.confidenceLabel(witness0)).toBe('0%');
|
||||
});
|
||||
});
|
||||
@@ -172,7 +172,10 @@
|
||||
<tr data-testid="witness-row">
|
||||
<td>
|
||||
<strong>{{ witness.witnessId }}</strong>
|
||||
<span class="subtle">{{ reachabilityLabel(witness) }}</span>
|
||||
<app-reachability-state-chip
|
||||
[state]="reachabilityState(witness)"
|
||||
[confidence]="witness.confidenceScore"
|
||||
></app-reachability-state-chip>
|
||||
</td>
|
||||
<td>{{ witness.cveId ?? witness.vulnId }}</td>
|
||||
<td>
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
readContextRouteState,
|
||||
} from '../../shared/ui/context-route-state/context-route-state';
|
||||
import { PoEDrawerComponent } from './poe-drawer.component';
|
||||
import { ReachabilityStateChipComponent, type ReachabilityState } from '../../shared/domain/reachability-state-chip/reachability-state-chip.component';
|
||||
import {
|
||||
type CoverageStatus,
|
||||
DEFAULT_REACHABILITY_SCAN_ID,
|
||||
@@ -71,6 +72,7 @@ const TIER_FILTERS: readonly TierFilter[] = [
|
||||
PoEDrawerComponent,
|
||||
ContextHeaderComponent,
|
||||
TabbedNavComponent,
|
||||
ReachabilityStateChipComponent,
|
||||
],
|
||||
templateUrl: './reachability-center.component.html',
|
||||
styleUrls: ['./reachability-center.component.scss'],
|
||||
@@ -358,6 +360,10 @@ export class ReachabilityCenterComponent implements OnInit {
|
||||
return witness.isReachable ? 'Reachable' : 'Unreachable';
|
||||
}
|
||||
|
||||
reachabilityState(witness: ReachabilityWitness): ReachabilityState {
|
||||
return witness.isReachable ? 'Reachable' : 'Unreachable';
|
||||
}
|
||||
|
||||
artifactRouteId(value: string): string {
|
||||
return value.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Unit tests for ReleasesListPageComponent -- DigestChipComponent adoption.
|
||||
* Sprint: SPRINT_20260308_013_FE_orphan_domain_signal_chips_adoption (FE-ODSC-004)
|
||||
*/
|
||||
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
import { ReleasesListPageComponent } from './releases-list-page.component';
|
||||
import { DigestChipComponent } from '../../shared/domain/digest-chip/digest-chip.component';
|
||||
|
||||
describe('ReleasesListPageComponent - DigestChip adoption', () => {
|
||||
let component: ReleasesListPageComponent;
|
||||
let fixture: ComponentFixture<ReleasesListPageComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ReleasesListPageComponent],
|
||||
providers: [provideRouter([])]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ReleasesListPageComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create the component', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render DigestChipComponent for each release row', () => {
|
||||
const digestChips = fixture.debugElement.queryAll(
|
||||
By.directive(DigestChipComponent)
|
||||
);
|
||||
// The component uses fixture data; verify at least one chip renders
|
||||
expect(digestChips.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should pass bundleDigest to each digest chip', () => {
|
||||
const digestChips = fixture.debugElement.queryAll(
|
||||
By.directive(DigestChipComponent)
|
||||
);
|
||||
for (const chip of digestChips) {
|
||||
const chipInstance = chip.componentInstance as DigestChipComponent;
|
||||
expect(chipInstance.digest).toBeTruthy();
|
||||
// Digests should contain sha256: prefix
|
||||
expect(chipInstance.digest).toContain('sha256:');
|
||||
}
|
||||
});
|
||||
|
||||
it('should use bundle variant for digest chips', () => {
|
||||
const digestChips = fixture.debugElement.queryAll(
|
||||
By.directive(DigestChipComponent)
|
||||
);
|
||||
for (const chip of digestChips) {
|
||||
const chipInstance = chip.componentInstance as DigestChipComponent;
|
||||
expect(chipInstance.variant).toBe('bundle');
|
||||
}
|
||||
});
|
||||
|
||||
it('should not have hand-rolled shortDigest method', () => {
|
||||
// Verify the old method was removed
|
||||
expect((component as any).shortDigest).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not have hand-rolled copyDigest method', () => {
|
||||
// Verify the old method was removed
|
||||
expect((component as any).copyDigest).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,7 @@ import { Component, ChangeDetectionStrategy, signal, computed } from '@angular/c
|
||||
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { DigestChipComponent } from '../../shared/domain/digest-chip/digest-chip.component';
|
||||
|
||||
interface Release {
|
||||
id: string;
|
||||
@@ -24,7 +25,7 @@ interface Release {
|
||||
@Component({
|
||||
selector: 'app-releases-list-page',
|
||||
standalone: true,
|
||||
imports: [RouterLink, FormsModule],
|
||||
imports: [RouterLink, FormsModule, DigestChipComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div class="releases-page">
|
||||
@@ -95,15 +96,10 @@ interface Release {
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<code class="digest" [title]="release.bundleDigest">
|
||||
{{ shortDigest(release.bundleDigest) }}
|
||||
</code>
|
||||
<button
|
||||
type="button"
|
||||
class="copy-btn"
|
||||
(click)="copyDigest(release.bundleDigest)"
|
||||
title="Copy digest"
|
||||
><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>
|
||||
<app-digest-chip
|
||||
[digest]="release.bundleDigest"
|
||||
variant="bundle"
|
||||
></app-digest-chip>
|
||||
</td>
|
||||
<td>{{ release.components }}</td>
|
||||
<td>{{ release.environment }}</td>
|
||||
@@ -315,14 +311,6 @@ export class ReleasesListPageComponent {
|
||||
});
|
||||
});
|
||||
|
||||
shortDigest(digest: string): string {
|
||||
const parts = digest.split(':');
|
||||
if (parts.length === 2 && parts[1].length > 10) {
|
||||
return `${parts[0]}:${parts[1].substring(0, 4)}...${parts[1].substring(parts[1].length - 3)}`;
|
||||
}
|
||||
return digest;
|
||||
}
|
||||
|
||||
onSearch(query: string): void {
|
||||
this.searchQuery.set(query);
|
||||
}
|
||||
@@ -363,14 +351,6 @@ export class ReleasesListPageComponent {
|
||||
this.gateFilter.set(select.value);
|
||||
}
|
||||
|
||||
async copyDigest(digest: string): Promise<void> {
|
||||
try {
|
||||
await navigator.clipboard.writeText(digest);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy digest:', err);
|
||||
}
|
||||
}
|
||||
|
||||
openCreateRelease(): void {
|
||||
console.log('Open create release modal');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user