Skip to content

Commit

Permalink
[PM-12998] View Cipher: Color Password (#12354)
Browse files Browse the repository at this point in the history
* show color password for visible passwords in vault view

- The password input will be visually hidden
- Adds tests for the login credentials component

* formatting
  • Loading branch information
nick-livefront authored Dec 20, 2024
1 parent acd3ab0 commit b27a1a5
Show file tree
Hide file tree
Showing 2 changed files with 218 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,34 @@ <h2 bitTypography="h6">{{ "loginCredentials" | i18n }}</h2>
></button>
</bit-form-field>
<bit-form-field *ngIf="cipher.login.password">
<bit-label [appTextDrag]="cipher.login.password">{{ "password" | i18n }}</bit-label>
<bit-label [appTextDrag]="cipher.login.password" id="password-label">
{{ "password" | i18n }}
</bit-label>
<input
id="password"
[ngClass]="{ 'tw-hidden': passwordRevealed }"
readonly
bitInput
type="password"
[value]="cipher.login.password"
aria-readonly="true"
data-testid="login-password"
class="tw-font-mono"
/>
<!-- Use a wrapping span to "recreate" a readonly input as close as possible -->
<span
*ngIf="passwordRevealed"
role="textbox"
tabindex="0"
data-testid="login-password-color"
aria-readonly="true"
[attr.aria-label]="cipher.login.password"
aria-labelledby="password-label"
>
<bit-color-password
class="tw-font-mono"
[password]="cipher.login.password"
></bit-color-password>
</span>
<button
*ngIf="cipher.viewPassword && passwordRevealed"
bitIconButton="bwi-numbered-list"
Expand Down Expand Up @@ -74,7 +91,7 @@ <h2 bitTypography="h6">{{ "loginCredentials" | i18n }}</h2>
</bit-form-field>
<div
*ngIf="showPasswordCount && passwordRevealed"
[ngClass]="{ 'tw-mt-3': !cipher.login.totp }"
[ngClass]="{ 'tw-mt-3': !cipher.login.totp, 'tw-mb-2': true }"
>
<bit-color-password
[password]="cipher.login.password"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { DebugElement } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { By } from "@angular/platform-browser";
import { mock } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";

import { CopyClickDirective } from "@bitwarden/angular/directives/copy-click.directive";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { EventType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { Fido2CredentialView } from "@bitwarden/common/vault/models/view/fido2-credential.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import { BitFormFieldComponent, ToastService } from "@bitwarden/components";
import { ColorPasswordComponent } from "@bitwarden/components/src/color-password/color-password.component";
import { BitPasswordInputToggleDirective } from "@bitwarden/components/src/form-field/password-input-toggle.directive";

import { LoginCredentialsViewComponent } from "./login-credentials-view.component";

describe("LoginCredentialsViewComponent", () => {
let component: LoginCredentialsViewComponent;
let fixture: ComponentFixture<LoginCredentialsViewComponent>;

const hasPremiumFromAnySource$ = new BehaviorSubject<boolean>(true);

const cipher = {
id: "cipher-id",
name: "Mock Cipher",
type: CipherType.Login,
login: new LoginView(),
} as CipherView;

cipher.login.password = "cipher-password";
cipher.login.username = "cipher-username";
const date = new Date("2024-02-02");
cipher.login.fido2Credentials = [{ creationDate: date } as Fido2CredentialView];

const collect = jest.fn();

beforeEach(async () => {
collect.mockClear();

await TestBed.configureTestingModule({
providers: [
{
provide: BillingAccountProfileStateService,
useValue: mock<BillingAccountProfileStateService>({ hasPremiumFromAnySource$ }),
},
{ provide: PremiumUpgradePromptService, useValue: mock<PremiumUpgradePromptService>() },
{ provide: EventCollectionService, useValue: mock<EventCollectionService>({ collect }) },
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
{ provide: ToastService, useValue: mock<ToastService>() },
{ provide: I18nService, useValue: { t: (...keys: string[]) => keys.join(" ") } },
],
}).compileComponents();

fixture = TestBed.createComponent(LoginCredentialsViewComponent);
component = fixture.componentInstance;
component.cipher = cipher;
fixture.detectChanges();
});

describe("username", () => {
let usernameField: DebugElement;

beforeEach(() => {
usernameField = fixture.debugElement.queryAll(By.directive(BitFormFieldComponent))[0];
});

it("displays the username", () => {
const usernameInput = usernameField.query(By.css("input")).nativeElement;

expect(usernameInput.value).toBe(cipher.login.username);
});

it("configures CopyClickDirective for the username", () => {
const usernameCopyButton = usernameField.query(By.directive(CopyClickDirective));
const usernameCopyClickDirective = usernameCopyButton.injector.get(CopyClickDirective);

expect(usernameCopyClickDirective.valueToCopy).toBe(cipher.login.username);
});
});

describe("password", () => {
let passwordField: DebugElement;

beforeEach(() => {
passwordField = fixture.debugElement.queryAll(By.directive(BitFormFieldComponent))[1];
});

it("displays the password", () => {
const passwordInput = passwordField.query(By.css("input")).nativeElement;

expect(passwordInput.value).toBe(cipher.login.password);
});

describe("copy", () => {
it("does not allow copy when `viewPassword` is false", () => {
cipher.viewPassword = false;
fixture.detectChanges();

const passwordCopyButton = passwordField.query(By.directive(CopyClickDirective));

expect(passwordCopyButton).toBeNull();
});

it("configures CopyClickDirective for the password", () => {
cipher.viewPassword = true;
fixture.detectChanges();

const passwordCopyButton = passwordField.query(By.directive(CopyClickDirective));
const passwordCopyClickDirective = passwordCopyButton.injector.get(CopyClickDirective);

expect(passwordCopyClickDirective.valueToCopy).toBe(cipher.login.password);
});
});

describe("toggle password", () => {
it("does not allow password to be viewed when `viewPassword` is false", () => {
cipher.viewPassword = false;
fixture.detectChanges();

const viewPasswordButton = passwordField.query(
By.directive(BitPasswordInputToggleDirective),
);

expect(viewPasswordButton).toBeNull();
});

it("shows password color component", () => {
cipher.viewPassword = true;
fixture.detectChanges();

const viewPasswordButton = passwordField.query(
By.directive(BitPasswordInputToggleDirective),
);
const toggleInputDirective = viewPasswordButton.injector.get(
BitPasswordInputToggleDirective,
);

toggleInputDirective.onClick();
fixture.detectChanges();

const passwordColor = passwordField.query(By.directive(ColorPasswordComponent));

expect(passwordColor.componentInstance.password).toBe(cipher.login.password);
});

it("records event", () => {
cipher.viewPassword = true;
fixture.detectChanges();

const viewPasswordButton = passwordField.query(
By.directive(BitPasswordInputToggleDirective),
);
const toggleInputDirective = viewPasswordButton.injector.get(
BitPasswordInputToggleDirective,
);

toggleInputDirective.onClick();
fixture.detectChanges();

expect(collect).toHaveBeenCalledWith(
EventType.Cipher_ClientToggledPasswordVisible,
cipher.id,
false,
cipher.organizationId,
);
});
});
});

describe("fido2Credentials", () => {
let fido2Field: DebugElement;

beforeEach(() => {
fido2Field = fixture.debugElement.queryAll(By.directive(BitFormFieldComponent))[2];

// Mock datePipe to avoid timezone related issues within tests
jest.spyOn(component["datePipe"], "transform").mockReturnValue("2/2/24 6:00PM");
fixture.detectChanges();
});

afterEach(() => {
jest.restoreAllMocks();
});

it("displays the creation date", () => {
const fido2Input = fido2Field.query(By.css("input")).nativeElement;

expect(fido2Input.value).toBe("dateCreated 2/2/24 6:00PM");
});
});
});

0 comments on commit b27a1a5

Please sign in to comment.