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);