Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions projects/wc/_mocks_/ui5-mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,9 @@ jest.mock('@ui5/webcomponents-ngx', () => {
jest.mock('@ui5/webcomponents-icons/dist/copy.js', () => ({}), {
virtual: true,
});
jest.mock('@ui5/webcomponents-icons/dist/hide.js', () => ({}), {
virtual: true,
});
jest.mock('@ui5/webcomponents-icons/dist/show.js', () => ({}), {
virtual: true,
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
<span class="secret-value">
<span class="masked">{{ maskedValue() }}</span>
<span class="original">{{ value() }}</span>
<span class="value-text">
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need the span in span and I don't see the value-text class

@if (isVisible()) {
<span class="original">{{ value() }}</span>
} @else {
<span class="masked">{{ maskedValue() }}</span>
}
</span>
</span>
Original file line number Diff line number Diff line change
@@ -1,27 +1,10 @@
.secret-value {
position: relative;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
user-select: none;
display: inline-flex;

.original {
display: none;
}
align-items: center;
gap: 0.25rem;

.masked {
display: inline;
transform: translateY(0.2em);
}

&:hover,
&:active {
.original {
display: inline;
}

.masked {
display: none;
}
display: flex;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { SecretValueComponent } from './secret-value.component';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';

describe('SecretValueComponent', () => {
Expand All @@ -20,7 +19,7 @@ describe('SecretValueComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [SecretValueComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
schemas: [],
});
});

Expand All @@ -29,6 +28,11 @@ describe('SecretValueComponent', () => {
expect(component).toBeTruthy();
});

it('should initialize with isVisible as false', () => {
const { component } = makeComponent('test-secret');
expect(component.isVisible()).toBe(false);
});

it('should mask value with asterisks', () => {
const { component, fixture } = makeComponent('my-secret-password');
const compiled = fixture.nativeElement;
Expand Down Expand Up @@ -68,13 +72,65 @@ describe('SecretValueComponent', () => {
);
});

it('should display original value in hidden span', () => {
it('should display masked value by default', () => {
const { fixture } = makeComponent('secret-value');
const compiled = fixture.nativeElement;

const maskedSpan = compiled.querySelector('.masked');
const originalSpan = compiled.querySelector('.original');

expect(maskedSpan).toBeTruthy();
expect(originalSpan).toBeFalsy();
});

it('should display original value when isVisible is true', () => {
const { component, fixture } = makeComponent('secret-value');

fixture.componentRef.setInput('isVisible', true);
fixture.detectChanges();

const compiled = fixture.nativeElement;
const originalSpan = compiled.querySelector('.original');
const maskedSpan = compiled.querySelector('.masked');

expect(originalSpan).toBeTruthy();
expect(originalSpan?.textContent).toBe('secret-value');
expect(maskedSpan).toBeFalsy();
});

it('should switch from masked to original when isVisible changes', () => {
const { component, fixture } = makeComponent('secret-value');
const compiled = fixture.nativeElement;

expect(component.isVisible()).toBe(false);
expect(compiled.querySelector('.masked')).toBeTruthy();
expect(compiled.querySelector('.original')).toBeFalsy();

fixture.componentRef.setInput('isVisible', true);
fixture.detectChanges();

expect(component.isVisible()).toBe(true);
expect(compiled.querySelector('.original')).toBeTruthy();
expect(compiled.querySelector('.masked')).toBeFalsy();
});

it('should switch back to masked when isVisible changes to false', () => {
const { component, fixture } = makeComponent('secret-value');

fixture.componentRef.setInput('isVisible', true);
fixture.detectChanges();
expect(component.isVisible()).toBe(true);

fixture.componentRef.setInput('isVisible', false);
fixture.detectChanges();

const compiled = fixture.nativeElement;
const maskedSpan = compiled.querySelector('.masked');
const originalSpan = compiled.querySelector('.original');

expect(component.isVisible()).toBe(false);
expect(maskedSpan).toBeTruthy();
expect(originalSpan).toBeFalsy();
});

it('should update masked value when input changes', () => {
Expand All @@ -86,4 +142,21 @@ describe('SecretValueComponent', () => {

expect(component.maskedValue()).toBe('*'.repeat('updated-secret'.length));
});

it('should maintain visibility state when input changes', () => {
const { component, fixture } = makeComponent('initial');

fixture.componentRef.setInput('isVisible', true);
fixture.detectChanges();

expect(component.isVisible()).toBe(true);

fixture.componentRef.setInput('value', 'updated-secret');
fixture.detectChanges();

expect(component.isVisible()).toBe(true);
const compiled = fixture.nativeElement;
const originalSpan = compiled.querySelector('.original');
expect(originalSpan?.textContent).toBe('updated-secret');
});
});
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Component, computed, input } from '@angular/core';


@Component({
selector: 'wc-secret-value',
imports: [],
schemas: [],
templateUrl: './secret-value.component.html',
styleUrl: './secret-value.component.scss',
})
export class SecretValueComponent {
value = input.required<string>();
isVisible = input<boolean>(false);
maskedValue = computed(() => '*'.repeat(this.value().length || 8));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
@if (displayAs() === 'plainText') {
{{ value() }}
} @else if (displayAs() === 'secret') {
<wc-secret-value [value]="value()"></wc-secret-value>
<wc-secret-value [value]="value()" [isVisible]="isVisible()"></wc-secret-value>
} @else if (isBoolLike()) {
<wc-boolean-value [boolValue]="boolValue()!"></wc-boolean-value>
} @else if (isUrlValue()) {
Expand All @@ -19,7 +19,15 @@
}
</span>

@if (displayAs() === 'secret') {
<ui5-icon
class="toggle-icon"
[name]="isVisible() ? 'hide' : 'show'"
(click)="toggleVisibility($event)"
></ui5-icon>
}

@if (withCopyButton()) {
<ui5-icon (click)="copyValue($event)" name="copy">
<ui5-icon class="toggle-icon" (click)="copyValue($event)" name="copy">
</ui5-icon>
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
:host {
display: flex;
align-items: center;
gap: 0.5rem;
}

.label-value {
Expand All @@ -15,4 +14,14 @@
font-size: 0.875rem;
line-height: 1.4rem;
margin: 3px 0;
}
}

.toggle-icon {
cursor: pointer;
transition: color 0.2s ease;
padding: 0.25rem;

&:hover {
color: var(--sapButton_Hover_TextColor, #0854a0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,93 @@ describe('ValueCellComponent', () => {
expect(compiled.querySelector('wc-secret-value')).toBeFalsy();
expect(component.displayAs()).toBeUndefined();
});

it('should render toggle icon when displayAs is secret', () => {
const { fixture } = makeComponent('secret-password', {
uiSettings: { displayAs: 'secret' },
});
const compiled = fixture.nativeElement;

const toggleIcon = compiled.querySelector('ui5-icon.toggle-icon');
expect(toggleIcon).toBeTruthy();
});

it('should not render toggle icon when displayAs is not secret', () => {
const { fixture } = makeComponent('plain-text', {
uiSettings: { displayAs: 'plainText' },
});
const compiled = fixture.nativeElement;

const toggleIcon = compiled.querySelector('ui5-icon.toggle-icon');
expect(toggleIcon).toBeFalsy();
});

it('should initialize isVisible as false', () => {
const { component } = makeComponent('secret-password', {
uiSettings: { displayAs: 'secret' },
});

expect(component.isVisible()).toBe(false);
});

it('should toggle visibility when icon is clicked', () => {
const { component, fixture } = makeComponent('secret-password', {
uiSettings: { displayAs: 'secret' },
});
const compiled = fixture.nativeElement;

expect(component.isVisible()).toBe(false);

const icon = compiled.querySelector('ui5-icon.toggle-icon');
icon?.click();
fixture.detectChanges();

expect(component.isVisible()).toBe(true);
});

it('should toggle back to hidden when icon is clicked again', () => {
const { component, fixture } = makeComponent('secret-password', {
uiSettings: { displayAs: 'secret' },
});
const compiled = fixture.nativeElement;

const icon = compiled.querySelector('ui5-icon.toggle-icon');

icon?.click();
fixture.detectChanges();
expect(component.isVisible()).toBe(true);

icon?.click();
fixture.detectChanges();
expect(component.isVisible()).toBe(false);
});

it('should stop event propagation when toggle icon is clicked', () => {
const { component, fixture } = makeComponent('secret-password', {
uiSettings: { displayAs: 'secret' },
});

const event = new Event('click');
const stopPropagationSpy = jest.spyOn(event, 'stopPropagation');

component.toggleVisibility(event);
fixture.detectChanges();

expect(stopPropagationSpy).toHaveBeenCalled();
});

it('should pass isVisible state to secret-value component', () => {
const { component, fixture } = makeComponent('secret-password', {
uiSettings: { displayAs: 'secret' },
});
const compiled = fixture.nativeElement;

component.isVisible.set(true);
fixture.detectChanges();

const secretValueComponent = compiled.querySelector('wc-secret-value');
expect(secretValueComponent).toBeTruthy();
});
});

describe('withCopyButton functionality', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Component,
computed,
input,
signal,
} from '@angular/core';
import { LuigiClient } from '@luigi-project/client/luigi-element';
import {
Expand All @@ -15,6 +16,8 @@ import {
import { Resource } from '@platform-mesh/portal-ui-lib/models/models/resource';
import { getResourceValueByJsonPath } from '@platform-mesh/portal-ui-lib/utils/utils';
import '@ui5/webcomponents-icons/dist/copy.js';
import '@ui5/webcomponents-icons/dist/hide.js';
import '@ui5/webcomponents-icons/dist/show.js';
import { IconComponent } from '@ui5/webcomponents-ngx';

@Component({
Expand Down Expand Up @@ -51,6 +54,12 @@ export class ValueCellComponent {

boolValue = computed(() => this.normalizeBoolean(this.value()));
stringValue = computed(() => this.normalizeString(this.value()));
isVisible = signal(false);

toggleVisibility(e: Event): void {
e.stopPropagation();
this.isVisible.set(!this.isVisible());
}

private normalizeBoolean(value: unknown): boolean | undefined {
const normalizedValue = value?.toString()?.toLowerCase();
Expand Down