diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.html index 9e29f1f1294..ccff7313258 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.html @@ -3,13 +3,14 @@ slot="header" [backAction]="close" showBackButton - [pageTitle]="title" + [pageTitle]="titleKey | i18n" > @@ -19,6 +20,7 @@ buttonType="primary" (click)="selectValue()" data-testid="select-button" + [disabled]="!(selectButtonText && generatedValue)" > {{ selectButtonText }} diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.spec.ts index 3255593a424..9c94f8fc63f 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.spec.ts @@ -1,15 +1,18 @@ import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog"; import { Component, EventEmitter, Input, Output } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; import { NoopAnimationsModule } from "@angular/platform-browser/animations"; import { mock, MockProxy } from "jest-mock-extended"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { AlgorithmInfo } from "@bitwarden/generator-core"; import { CipherFormGeneratorComponent } from "@bitwarden/vault"; import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service"; import { + GeneratorDialogAction, GeneratorDialogParams, GeneratorDialogResult, VaultGeneratorDialogComponent, @@ -21,8 +24,9 @@ import { standalone: true, }) class MockCipherFormGenerator { - @Input() type: "password" | "username"; - @Input() uri: string; + @Input() type: "password" | "username" = "password"; + @Output() algorithmSelected: EventEmitter = new EventEmitter(); + @Input() uri: string = ""; @Output() valueGenerated = new EventEmitter(); } @@ -53,34 +57,87 @@ describe("VaultGeneratorDialogComponent", () => { fixture = TestBed.createComponent(VaultGeneratorDialogComponent); component = fixture.componentInstance; - }); - - it("should create", () => { fixture.detectChanges(); - expect(component).toBeTruthy(); }); - it("should use the appropriate text based on generator type", () => { - expect(component["title"]).toBe("passwordGenerator"); - expect(component["selectButtonText"]).toBe("useThisPassword"); - - dialogData.type = "username"; - - fixture = TestBed.createComponent(VaultGeneratorDialogComponent); - component = fixture.componentInstance; - - expect(component["title"]).toBe("usernameGenerator"); - expect(component["selectButtonText"]).toBe("useThisUsername"); + it("should show password generator title", () => { + const header = fixture.debugElement.query(By.css("popup-header")).componentInstance; + expect(header.pageTitle).toBe("passwordGenerator"); }); - it("should close the dialog with the generated value when the user selects it", () => { - component["generatedValue"] = "generated-value"; + it("should pass type to cipher form generator", () => { + const generator = fixture.debugElement.query( + By.css("vault-cipher-form-generator"), + ).componentInstance; + expect(generator.type).toBe("password"); + }); - fixture.nativeElement.querySelector("button[data-testid='select-button']").click(); + it("should enable select button when value is generated", () => { + component.onAlgorithmSelected({ useGeneratedValue: "Test" } as any); + component.onValueGenerated("test-password"); + fixture.detectChanges(); + + const button = fixture.debugElement.query( + By.css("[data-testid='select-button']"), + ).nativeElement; + expect(button.disabled).toBe(false); + }); + + it("should disable the button if no value has been generated", () => { + const generator = fixture.debugElement.query( + By.css("vault-cipher-form-generator"), + ).componentInstance; + + generator.algorithmSelected.emit({ useGeneratedValue: "Use Password" } as any); + fixture.detectChanges(); + + const button = fixture.debugElement.query( + By.css("[data-testid='select-button']"), + ).nativeElement; + expect(button.disabled).toBe(true); + }); + + it("should disable the button if no algorithm is selected", () => { + const generator = fixture.debugElement.query( + By.css("vault-cipher-form-generator"), + ).componentInstance; + + generator.valueGenerated.emit("test-password"); + fixture.detectChanges(); + + const button = fixture.debugElement.query( + By.css("[data-testid='select-button']"), + ).nativeElement; + expect(button.disabled).toBe(true); + }); + + it("should update button text when algorithm is selected", () => { + component.onAlgorithmSelected({ useGeneratedValue: "Use This Password" } as any); + fixture.detectChanges(); + + const button = fixture.debugElement.query( + By.css("[data-testid='select-button']"), + ).nativeElement; + expect(button.textContent.trim()).toBe("Use This Password"); + }); + + it("should close with generated value when selected", () => { + component.onAlgorithmSelected({ useGeneratedValue: "Test" } as any); + component.onValueGenerated("test-password"); + fixture.detectChanges(); + + fixture.debugElement.query(By.css("[data-testid='select-button']")).nativeElement.click(); expect(mockDialogRef.close).toHaveBeenCalledWith({ - action: "selected", - generatedValue: "generated-value", + action: GeneratorDialogAction.Selected, + generatedValue: "test-password", + }); + }); + + it("should close with canceled action when dismissed", () => { + fixture.debugElement.query(By.css("popup-header")).componentInstance.backAction(); + expect(mockDialogRef.close).toHaveBeenCalledWith({ + action: GeneratorDialogAction.Canceled, }); }); }); diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.ts index 9e6750004d8..0eeb2e95a29 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.ts @@ -7,6 +7,8 @@ import { Component, Inject } from "@angular/core"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ButtonModule, DialogService } from "@bitwarden/components"; +import { AlgorithmInfo } from "@bitwarden/generator-core"; +import { I18nPipe } from "@bitwarden/ui-common"; import { CipherFormGeneratorComponent } from "@bitwarden/vault"; import { PopupFooterComponent } from "../../../../../platform/popup/layout/popup-footer.component"; @@ -39,13 +41,12 @@ export enum GeneratorDialogAction { CommonModule, CipherFormGeneratorComponent, ButtonModule, + I18nPipe, ], }) export class VaultGeneratorDialogComponent { - protected title = this.i18nService.t(this.isPassword ? "passwordGenerator" : "usernameGenerator"); - protected selectButtonText = this.i18nService.t( - this.isPassword ? "useThisPassword" : "useThisUsername", - ); + protected selectButtonText: string | undefined; + protected titleKey = this.isPassword ? "passwordGenerator" : "usernameGenerator"; /** * Whether the dialog is generating a password/passphrase. If false, it is generating a username. @@ -92,6 +93,16 @@ export class VaultGeneratorDialogComponent { this.generatedValue = value; } + onAlgorithmSelected = (selected?: AlgorithmInfo) => { + if (selected) { + this.selectButtonText = selected.useGeneratedValue; + } else { + // default to email + this.selectButtonText = this.i18nService.t("useThisEmail"); + } + this.generatedValue = undefined; + }; + /** * Opens the vault generator dialog in a full screen dialog. */ diff --git a/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.html b/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.html index 84e64956ca5..47232dff66d 100644 --- a/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.html +++ b/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.html @@ -4,7 +4,7 @@ diff --git a/apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.spec.ts b/apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.spec.ts index 41f2c7d8348..11a97a1f343 100644 --- a/apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.spec.ts +++ b/apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.spec.ts @@ -1,19 +1,19 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; -import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog"; +import { Component, Input, Output, EventEmitter } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; import { NoopAnimationsModule } from "@angular/platform-browser/animations"; import { mock, MockProxy } from "jest-mock-extended"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { AlgorithmInfo } from "@bitwarden/generator-core"; import { CipherFormGeneratorComponent } from "@bitwarden/vault"; import { WebVaultGeneratorDialogAction, WebVaultGeneratorDialogComponent, - WebVaultGeneratorDialogParams, + WebVaultGeneratorDialogResult, } from "./web-generator-dialog.component"; @Component({ @@ -22,7 +22,8 @@ import { standalone: true, }) class MockCipherFormGenerator { - @Input() type: "password" | "username"; + @Input() type: "password" | "username" = "password"; + @Output() algorithmSelected: EventEmitter = new EventEmitter(); @Input() uri?: string; @Output() valueGenerated = new EventEmitter(); } @@ -30,35 +31,20 @@ class MockCipherFormGenerator { describe("WebVaultGeneratorDialogComponent", () => { let component: WebVaultGeneratorDialogComponent; let fixture: ComponentFixture; - - let dialogRef: MockProxy>; + let dialogRef: MockProxy>; let mockI18nService: MockProxy; beforeEach(async () => { - dialogRef = mock>(); + dialogRef = mock>(); mockI18nService = mock(); - const mockDialogData: WebVaultGeneratorDialogParams = { type: "password" }; - await TestBed.configureTestingModule({ imports: [NoopAnimationsModule, WebVaultGeneratorDialogComponent], providers: [ - { - provide: DialogRef, - useValue: dialogRef, - }, - { - provide: DIALOG_DATA, - useValue: mockDialogData, - }, - { - provide: I18nService, - useValue: mockI18nService, - }, - { - provide: PlatformUtilsService, - useValue: mock(), - }, + { provide: DialogRef, useValue: dialogRef }, + { provide: DIALOG_DATA, useValue: { type: "password" } }, + { provide: I18nService, useValue: mockI18nService }, + { provide: PlatformUtilsService, useValue: mock() }, ], }) .overrideComponent(WebVaultGeneratorDialogComponent, { @@ -72,38 +58,73 @@ describe("WebVaultGeneratorDialogComponent", () => { fixture.detectChanges(); }); - it("initializes without errors", () => { - fixture.detectChanges(); + it("should create", () => { expect(component).toBeTruthy(); }); - it("closes the dialog with 'canceled' result when close is called", () => { - const closeSpy = jest.spyOn(dialogRef, "close"); + it("should enable button when value and algorithm are selected", () => { + const generator = fixture.debugElement.query( + By.css("vault-cipher-form-generator"), + ).componentInstance; - (component as any).close(); + generator.algorithmSelected.emit({ useGeneratedValue: "Use Password" } as any); + generator.valueGenerated.emit("test-password"); + fixture.detectChanges(); - expect(closeSpy).toHaveBeenCalledWith({ + const button = fixture.debugElement.query( + By.css("[data-testid='select-button']"), + ).nativeElement; + expect(button.disabled).toBe(false); + }); + + it("should disable the button if no value has been generated", () => { + const generator = fixture.debugElement.query( + By.css("vault-cipher-form-generator"), + ).componentInstance; + + generator.algorithmSelected.emit({ useGeneratedValue: "Use Password" } as any); + fixture.detectChanges(); + + const button = fixture.debugElement.query( + By.css("[data-testid='select-button']"), + ).nativeElement; + expect(button.disabled).toBe(true); + }); + + it("should disable the button if no algorithm is selected", () => { + const generator = fixture.debugElement.query( + By.css("vault-cipher-form-generator"), + ).componentInstance; + + generator.valueGenerated.emit("test-password"); + fixture.detectChanges(); + + const button = fixture.debugElement.query( + By.css("[data-testid='select-button']"), + ).nativeElement; + expect(button.disabled).toBe(true); + }); + + it("should close with selected value when confirmed", () => { + const generator = fixture.debugElement.query( + By.css("vault-cipher-form-generator"), + ).componentInstance; + generator.algorithmSelected.emit({ useGeneratedValue: "Use Password" } as any); + generator.valueGenerated.emit("test-password"); + fixture.detectChanges(); + + fixture.debugElement.query(By.css("[data-testid='select-button']")).nativeElement.click(); + + expect(dialogRef.close).toHaveBeenCalledWith({ + action: WebVaultGeneratorDialogAction.Selected, + generatedValue: "test-password", + }); + }); + + it("should close with canceled action when dismissed", () => { + component["close"](); + expect(dialogRef.close).toHaveBeenCalledWith({ action: WebVaultGeneratorDialogAction.Canceled, }); }); - - it("closes the dialog with 'selected' result when selectValue is called", () => { - const closeSpy = jest.spyOn(dialogRef, "close"); - const generatedValue = "generated-value"; - component.onValueGenerated(generatedValue); - - (component as any).selectValue(); - - expect(closeSpy).toHaveBeenCalledWith({ - action: WebVaultGeneratorDialogAction.Selected, - generatedValue: generatedValue, - }); - }); - - it("updates generatedValue when onValueGenerated is called", () => { - const generatedValue = "new-generated-value"; - component.onValueGenerated(generatedValue); - - expect((component as any).generatedValue).toBe(generatedValue); - }); }); diff --git a/apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.ts b/apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.ts index a87bcb85804..b0e5514ce21 100644 --- a/apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.ts +++ b/apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.ts @@ -6,6 +6,8 @@ import { Component, Inject } from "@angular/core"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components"; +import { AlgorithmInfo } from "@bitwarden/generator-core"; +import { I18nPipe } from "@bitwarden/ui-common"; import { CipherFormGeneratorComponent } from "@bitwarden/vault"; export interface WebVaultGeneratorDialogParams { @@ -27,13 +29,11 @@ export enum WebVaultGeneratorDialogAction { selector: "web-vault-generator-dialog", templateUrl: "./web-generator-dialog.component.html", standalone: true, - imports: [CommonModule, CipherFormGeneratorComponent, ButtonModule, DialogModule], + imports: [CommonModule, CipherFormGeneratorComponent, ButtonModule, DialogModule, I18nPipe], }) export class WebVaultGeneratorDialogComponent { - protected title = this.i18nService.t(this.isPassword ? "passwordGenerator" : "usernameGenerator"); - protected selectButtonText = this.i18nService.t( - this.isPassword ? "useThisPassword" : "useThisUsername", - ); + protected titleKey = this.isPassword ? "passwordGenerator" : "usernameGenerator"; + protected buttonLabel: string | undefined; /** * Whether the dialog is generating a password/passphrase. If false, it is generating a username. @@ -80,6 +80,16 @@ export class WebVaultGeneratorDialogComponent { this.generatedValue = value; } + onAlgorithmSelected = (selected?: AlgorithmInfo) => { + if (selected) { + this.buttonLabel = selected.useGeneratedValue; + } else { + // default to email + this.buttonLabel = this.i18nService.t("useThisEmail"); + } + this.generatedValue = undefined; + }; + /** * Opens the vault generator dialog. */ diff --git a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts index 2d539b7ba3a..b9e5ed3c0ab 100644 --- a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts +++ b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { CommonModule } from "@angular/common"; import { Component, EventEmitter, Input, Output } from "@angular/core"; @@ -18,9 +16,6 @@ import { AlgorithmInfo, GeneratedCredential } from "@bitwarden/generator-core"; imports: [CommonModule, GeneratorModule], }) export class CipherFormGeneratorComponent { - @Input() - onAlgorithmSelected: (selected: AlgorithmInfo) => void; - @Input() uri: string = ""; @@ -28,17 +23,25 @@ export class CipherFormGeneratorComponent { * The type of generator form to show. */ @Input({ required: true }) - type: "password" | "username"; + type: "password" | "username" = "password"; /** Removes bottom margin of internal sections */ @Input({ transform: coerceBooleanProperty }) disableMargin = false; + @Output() + algorithmSelected = new EventEmitter(); + /** * Emits an event when a new value is generated. */ @Output() valueGenerated = new EventEmitter(); + /** Event handler for when an algorithm is selected */ + onAlgorithmSelected = (selected: AlgorithmInfo) => { + this.algorithmSelected.emit(selected); + }; + /** Event handler for both generation components */ onCredentialGenerated = (generatedCred: GeneratedCredential) => { this.valueGenerated.emit(generatedCred.credential);