1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-02-23 02:31:26 +01:00

[PM-12998] View Cipher: Color Password (#12354)

* show color password for visible passwords in vault view

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

* formatting
This commit is contained in:
Nick Krantz 2024-12-20 09:44:36 -06:00 committed by GitHub
parent acd3ab05f6
commit b27a1a5337
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 218 additions and 3 deletions

View File

@ -28,17 +28,34 @@
></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"
@ -74,7 +91,7 @@
</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"

View File

@ -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");
});
});
});