mirror of
https://github.com/bitwarden/browser
synced 2026-02-11 05:53:42 +00:00
Merge branch 'main' into auth/pm-26209/bugfix-desktop-error-on-auth-request-approval
This commit is contained in:
@@ -24,6 +24,8 @@ import { KeyService } from "@bitwarden/key-management";
|
||||
selector: "app-user-verification",
|
||||
standalone: false,
|
||||
})
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/PM-28232): Use Directive suffix
|
||||
// eslint-disable-next-line @angular-eslint/directive-class-suffix
|
||||
export class UserVerificationComponent implements ControlValueAccessor, OnInit, OnDestroy {
|
||||
private _invalidSecret = false;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
|
||||
@@ -45,6 +45,8 @@ export function _cipherListVirtualScrollStrategyFactory(cipherListDir: CipherLis
|
||||
},
|
||||
],
|
||||
})
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/PM-28232): Use Directive suffix
|
||||
// eslint-disable-next-line @angular-eslint/directive-class-suffix
|
||||
export class CipherListVirtualScroll extends CdkFixedSizeVirtualScroll {
|
||||
_scrollStrategy: CipherListVirtualScrollStrategy;
|
||||
|
||||
|
||||
@@ -102,7 +102,6 @@ import { MasterPasswordApiService as MasterPasswordApiServiceAbstraction } from
|
||||
import { PasswordResetEnrollmentServiceAbstraction } from "@bitwarden/common/auth/abstractions/password-reset-enrollment.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { UserVerificationApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification-api.service.abstraction";
|
||||
import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { WebAuthnLoginApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login-api.service.abstraction";
|
||||
@@ -125,13 +124,17 @@ import { OrganizationInviteService } from "@bitwarden/common/auth/services/organ
|
||||
import { PasswordResetEnrollmentServiceImplementation } from "@bitwarden/common/auth/services/password-reset-enrollment.service.implementation";
|
||||
import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/services/token.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service";
|
||||
import { UserVerificationApiService } from "@bitwarden/common/auth/services/user-verification/user-verification-api.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service";
|
||||
import { WebAuthnLoginApiService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login-api.service";
|
||||
import { WebAuthnLoginPrfKeyService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login-prf-key.service";
|
||||
import { WebAuthnLoginService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login.service";
|
||||
import { TwoFactorApiService, DefaultTwoFactorApiService } from "@bitwarden/common/auth/two-factor";
|
||||
import {
|
||||
TwoFactorApiService,
|
||||
DefaultTwoFactorApiService,
|
||||
TwoFactorService,
|
||||
DefaultTwoFactorService,
|
||||
} from "@bitwarden/common/auth/two-factor";
|
||||
import {
|
||||
AutofillSettingsService,
|
||||
AutofillSettingsServiceAbstraction,
|
||||
@@ -525,7 +528,7 @@ const safeProviders: SafeProvider[] = [
|
||||
KeyConnectorServiceAbstraction,
|
||||
EnvironmentService,
|
||||
StateServiceAbstraction,
|
||||
TwoFactorServiceAbstraction,
|
||||
TwoFactorService,
|
||||
I18nServiceAbstraction,
|
||||
EncryptService,
|
||||
PasswordStrengthServiceAbstraction,
|
||||
@@ -1169,9 +1172,14 @@ const safeProviders: SafeProvider[] = [
|
||||
deps: [StateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: TwoFactorServiceAbstraction,
|
||||
useClass: TwoFactorService,
|
||||
deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction, GlobalStateProvider],
|
||||
provide: TwoFactorService,
|
||||
useClass: DefaultTwoFactorService,
|
||||
deps: [
|
||||
I18nServiceAbstraction,
|
||||
PlatformUtilsServiceAbstraction,
|
||||
GlobalStateProvider,
|
||||
TwoFactorApiService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: FormValidationErrorsServiceAbstraction,
|
||||
|
||||
@@ -3,20 +3,20 @@
|
||||
>
|
||||
<div class="tw-flex tw-justify-between tw-items-start tw-flex-grow">
|
||||
<div>
|
||||
<h2 bitTypography="h4" class="tw-font-medium !tw-mb-1">{{ title }}</h2>
|
||||
<h2 *ngIf="title()" bitTypography="h4" class="tw-font-medium !tw-mb-1">{{ title() }}</h2>
|
||||
<p
|
||||
*ngIf="subtitle"
|
||||
*ngIf="subtitle()"
|
||||
class="tw-text-main tw-mb-0"
|
||||
bitTypography="body2"
|
||||
[innerHTML]="subtitle"
|
||||
[innerHTML]="subtitle()"
|
||||
></p>
|
||||
<ng-content *ngIf="!subtitle"></ng-content>
|
||||
<ng-content *ngIf="!subtitle()"></ng-content>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-close"
|
||||
size="small"
|
||||
*ngIf="!persistent"
|
||||
*ngIf="!persistent()"
|
||||
(click)="handleDismiss()"
|
||||
class="-tw-me-2"
|
||||
[label]="'close' | i18n"
|
||||
@@ -28,10 +28,10 @@
|
||||
bitButton
|
||||
type="button"
|
||||
buttonType="primary"
|
||||
*ngIf="buttonText"
|
||||
*ngIf="buttonText()"
|
||||
(click)="handleButtonClick($event)"
|
||||
>
|
||||
{{ buttonText }}
|
||||
<i *ngIf="buttonIcon" [ngClass]="buttonIcon" class="bwi tw-ml-1" aria-hidden="true"></i>
|
||||
{{ buttonText() }}
|
||||
<i *ngIf="buttonIcon()" [ngClass]="buttonIcon()" class="bwi tw-ml-1" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { ChangeDetectionStrategy, Component } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
import { SpotlightComponent } from "./spotlight.component";
|
||||
|
||||
describe("SpotlightComponent", () => {
|
||||
let fixture: ComponentFixture<SpotlightComponent>;
|
||||
let component: SpotlightComponent;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SpotlightComponent],
|
||||
providers: [{ provide: I18nService, useValue: { t: (key: string) => key } }],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(SpotlightComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
function detect(): void {
|
||||
fixture.detectChanges();
|
||||
}
|
||||
|
||||
it("should create", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe("rendering when inputs are null", () => {
|
||||
it("should render without crashing when inputs are null/undefined", () => {
|
||||
// Explicitly drive the inputs to null to exercise template null branches
|
||||
fixture.componentRef.setInput("title", null);
|
||||
fixture.componentRef.setInput("subtitle", null);
|
||||
fixture.componentRef.setInput("buttonText", null);
|
||||
fixture.componentRef.setInput("buttonIcon", null);
|
||||
// persistent has a default, but drive it as well for coverage sanity
|
||||
fixture.componentRef.setInput("persistent", false);
|
||||
|
||||
expect(() => detect()).not.toThrow();
|
||||
|
||||
const root = fixture.debugElement.nativeElement as HTMLElement;
|
||||
expect(root).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("close button visibility based on persistent", () => {
|
||||
it("should show the close button when persistent is false", () => {
|
||||
fixture.componentRef.setInput("persistent", false);
|
||||
detect();
|
||||
|
||||
// Assumes dismiss uses bitIconButton
|
||||
const dismissButton = fixture.debugElement.query(By.css("button[bitIconButton]"));
|
||||
|
||||
expect(dismissButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should hide the close button when persistent is true", () => {
|
||||
fixture.componentRef.setInput("persistent", true);
|
||||
detect();
|
||||
|
||||
const dismissButton = fixture.debugElement.query(By.css("button[bitIconButton]"));
|
||||
expect(dismissButton).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("event emission", () => {
|
||||
it("should emit onButtonClick when CTA button is clicked", () => {
|
||||
const clickSpy = jest.fn();
|
||||
component.onButtonClick.subscribe(clickSpy);
|
||||
|
||||
fixture.componentRef.setInput("buttonText", "Click me");
|
||||
detect();
|
||||
|
||||
const buttonDe = fixture.debugElement.query(By.css("button[bitButton]"));
|
||||
expect(buttonDe).toBeTruthy();
|
||||
|
||||
const event = new MouseEvent("click");
|
||||
buttonDe.triggerEventHandler("click", event);
|
||||
|
||||
expect(clickSpy).toHaveBeenCalledTimes(1);
|
||||
expect(clickSpy.mock.calls[0][0]).toBeInstanceOf(MouseEvent);
|
||||
});
|
||||
|
||||
it("should emit onDismiss when close button is clicked", () => {
|
||||
const dismissSpy = jest.fn();
|
||||
component.onDismiss.subscribe(dismissSpy);
|
||||
|
||||
fixture.componentRef.setInput("persistent", false);
|
||||
detect();
|
||||
|
||||
const dismissButton = fixture.debugElement.query(By.css("button[bitIconButton]"));
|
||||
expect(dismissButton).toBeTruthy();
|
||||
|
||||
dismissButton.triggerEventHandler("click", new MouseEvent("click"));
|
||||
|
||||
expect(dismissSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("handleButtonClick should emit via onButtonClick()", () => {
|
||||
const clickSpy = jest.fn();
|
||||
component.onButtonClick.subscribe(clickSpy);
|
||||
|
||||
const event = new MouseEvent("click");
|
||||
component.handleButtonClick(event);
|
||||
|
||||
expect(clickSpy).toHaveBeenCalledTimes(1);
|
||||
expect(clickSpy.mock.calls[0][0]).toBe(event);
|
||||
});
|
||||
|
||||
it("handleDismiss should emit via onDismiss()", () => {
|
||||
const dismissSpy = jest.fn();
|
||||
component.onDismiss.subscribe(dismissSpy);
|
||||
|
||||
component.handleDismiss();
|
||||
|
||||
expect(dismissSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("content projection behavior", () => {
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [SpotlightComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<bit-spotlight>
|
||||
<span class="tw-text-sm">Projected content</span>
|
||||
</bit-spotlight>
|
||||
`,
|
||||
})
|
||||
class HostWithProjectionComponent {}
|
||||
|
||||
let hostFixture: ComponentFixture<HostWithProjectionComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
hostFixture = TestBed.createComponent(HostWithProjectionComponent);
|
||||
});
|
||||
|
||||
it("should render projected content inside the spotlight", () => {
|
||||
hostFixture.detectChanges();
|
||||
|
||||
const projected = hostFixture.debugElement.query(By.css(".tw-text-sm"));
|
||||
expect(projected).toBeTruthy();
|
||||
expect(projected.nativeElement.textContent.trim()).toBe("Projected content");
|
||||
});
|
||||
});
|
||||
|
||||
describe("boolean attribute transform for persistent", () => {
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [CommonModule, SpotlightComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<!-- bare persistent attribute -->
|
||||
<bit-spotlight *ngIf="mode === 'bare'" persistent></bit-spotlight>
|
||||
|
||||
<!-- no persistent attribute -->
|
||||
<bit-spotlight *ngIf="mode === 'none'"></bit-spotlight>
|
||||
|
||||
<!-- explicit persistent="false" -->
|
||||
<bit-spotlight *ngIf="mode === 'falseStr'" persistent="false"></bit-spotlight>
|
||||
`,
|
||||
})
|
||||
class BooleanHostComponent {
|
||||
mode: "bare" | "none" | "falseStr" = "bare";
|
||||
}
|
||||
|
||||
let boolFixture: ComponentFixture<BooleanHostComponent>;
|
||||
let boolHost: BooleanHostComponent;
|
||||
|
||||
beforeEach(async () => {
|
||||
boolFixture = TestBed.createComponent(BooleanHostComponent);
|
||||
boolHost = boolFixture.componentInstance;
|
||||
});
|
||||
|
||||
function getSpotlight(): SpotlightComponent {
|
||||
const de = boolFixture.debugElement.query(By.directive(SpotlightComponent));
|
||||
return de.componentInstance as SpotlightComponent;
|
||||
}
|
||||
|
||||
it("treats bare 'persistent' attribute as true via booleanAttribute", () => {
|
||||
boolHost.mode = "bare";
|
||||
boolFixture.detectChanges();
|
||||
|
||||
const spotlight = getSpotlight();
|
||||
expect(spotlight.persistent()).toBe(true);
|
||||
});
|
||||
|
||||
it("uses default false when 'persistent' is omitted", () => {
|
||||
boolHost.mode = "none";
|
||||
boolFixture.detectChanges();
|
||||
|
||||
const spotlight = getSpotlight();
|
||||
expect(spotlight.persistent()).toBe(false);
|
||||
});
|
||||
|
||||
it('treats persistent="false" as false', () => {
|
||||
boolHost.mode = "falseStr";
|
||||
boolFixture.detectChanges();
|
||||
|
||||
const spotlight = getSpotlight();
|
||||
expect(spotlight.persistent()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,43 +1,28 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { booleanAttribute, ChangeDetectionStrategy, Component, input, output } from "@angular/core";
|
||||
|
||||
import { ButtonModule, IconButtonModule, TypographyModule } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "bit-spotlight",
|
||||
templateUrl: "spotlight.component.html",
|
||||
imports: [ButtonModule, CommonModule, IconButtonModule, I18nPipe, TypographyModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class SpotlightComponent {
|
||||
// The title of the component
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input({ required: true }) title: string | null = null;
|
||||
readonly title = input<string>();
|
||||
// The subtitle of the component
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() subtitle?: string | null = null;
|
||||
readonly subtitle = input<string>();
|
||||
// The text to display on the button
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() buttonText?: string;
|
||||
// Wheter the component can be dismissed, if true, the component will not show a close button
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() persistent = false;
|
||||
readonly buttonText = input<string>();
|
||||
// Whether the component can be dismissed, if true, the component will not show a close button
|
||||
readonly persistent = input(false, { transform: booleanAttribute });
|
||||
// Optional icon to display on the button
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() buttonIcon: string | null = null;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
|
||||
@Output() onDismiss = new EventEmitter<void>();
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
|
||||
@Output() onButtonClick = new EventEmitter();
|
||||
readonly buttonIcon = input<string>();
|
||||
readonly onDismiss = output<void>();
|
||||
readonly onButtonClick = output<MouseEvent>();
|
||||
|
||||
handleButtonClick(event: MouseEvent): void {
|
||||
this.onButtonClick.emit(event);
|
||||
|
||||
Reference in New Issue
Block a user