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:
parent
acd3ab05f6
commit
b27a1a5337
@ -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"
|
||||
|
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user