1
0
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:
rr-bw
2025-11-24 21:05:50 -08:00
247 changed files with 3909 additions and 1678 deletions

View File

@@ -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

View File

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

View File

@@ -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,

View File

@@ -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>

View File

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

View File

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