mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 05:43:41 +00:00
* feat(billing): add provided as a required property to premium response * fix(billing): replace hard coded storage variables with retrieved plan * tests(billing): add tests to pricing-summary service * feat(billing): add optional property. * fix(billing): update storage logic * fix(billing): remove optional check * fix(billing): remove optionality * fix(billing): remove optionality * fix(billing): refactored storage calculation logic * feat(billing): add provided amounts to subscription-pricing-service * fix(billing): update cloud premium component * fix(billing): update desktop premium component * fix(billing): update org plans component * fix(billing) update stories and tests * fix(billing): update messages * fix(billing): replace storage sizes * fix(billing): update messages * fix(billing): update components * fix(billing): update components for pricing and storage retrieval * fix(billing): revert self-hosted change
210 lines
7.3 KiB
TypeScript
210 lines
7.3 KiB
TypeScript
import { CdkTrapFocus } from "@angular/cdk/a11y";
|
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
|
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
|
import { firstValueFrom, of, throwError } from "rxjs";
|
|
|
|
import { SubscriptionPricingServiceAbstraction } from "@bitwarden/common/billing/abstractions/subscription-pricing.service.abstraction";
|
|
import {
|
|
PersonalSubscriptionPricingTier,
|
|
PersonalSubscriptionPricingTierIds,
|
|
SubscriptionCadenceIds,
|
|
} from "@bitwarden/common/billing/types/subscription-pricing-tier";
|
|
import {
|
|
EnvironmentService,
|
|
Region,
|
|
} from "@bitwarden/common/platform/abstractions/environment.service";
|
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
|
import { DialogRef, ToastService } from "@bitwarden/components";
|
|
import { LogService } from "@bitwarden/logging";
|
|
|
|
import { PremiumUpgradeDialogComponent } from "./premium-upgrade-dialog.component";
|
|
|
|
describe("PremiumUpgradeDialogComponent", () => {
|
|
let component: PremiumUpgradeDialogComponent;
|
|
let fixture: ComponentFixture<PremiumUpgradeDialogComponent>;
|
|
let mockDialogRef: jest.Mocked<DialogRef>;
|
|
let mockSubscriptionPricingService: jest.Mocked<SubscriptionPricingServiceAbstraction>;
|
|
let mockI18nService: jest.Mocked<I18nService>;
|
|
let mockToastService: jest.Mocked<ToastService>;
|
|
let mockEnvironmentService: jest.Mocked<EnvironmentService>;
|
|
let mockPlatformUtilsService: jest.Mocked<PlatformUtilsService>;
|
|
let mockLogService: jest.Mocked<LogService>;
|
|
|
|
const mockPremiumTier: PersonalSubscriptionPricingTier = {
|
|
id: PersonalSubscriptionPricingTierIds.Premium,
|
|
name: "Premium",
|
|
description: "Advanced features for power users",
|
|
availableCadences: [SubscriptionCadenceIds.Annually],
|
|
passwordManager: {
|
|
type: "standalone",
|
|
annualPrice: 10,
|
|
annualPricePerAdditionalStorageGB: 4,
|
|
providedStorageGB: 1,
|
|
features: [
|
|
{ key: "feature1", value: "Feature 1" },
|
|
{ key: "feature2", value: "Feature 2" },
|
|
{ key: "feature3", value: "Feature 3" },
|
|
],
|
|
},
|
|
};
|
|
|
|
const mockFamiliesTier: PersonalSubscriptionPricingTier = {
|
|
id: PersonalSubscriptionPricingTierIds.Families,
|
|
name: "Families",
|
|
description: "Family plan",
|
|
availableCadences: [SubscriptionCadenceIds.Annually],
|
|
passwordManager: {
|
|
type: "packaged",
|
|
users: 6,
|
|
annualPrice: 40,
|
|
annualPricePerAdditionalStorageGB: 4,
|
|
providedStorageGB: 1,
|
|
features: [{ key: "featureA", value: "Feature A" }],
|
|
},
|
|
};
|
|
|
|
beforeEach(async () => {
|
|
mockDialogRef = {
|
|
close: jest.fn(),
|
|
} as any;
|
|
|
|
mockSubscriptionPricingService = {
|
|
getPersonalSubscriptionPricingTiers$: jest.fn(),
|
|
} as any;
|
|
|
|
mockI18nService = {
|
|
t: jest.fn((key: string) => key),
|
|
} as any;
|
|
|
|
mockToastService = {
|
|
showToast: jest.fn(),
|
|
} as any;
|
|
|
|
mockEnvironmentService = {
|
|
environment$: of({
|
|
getWebVaultUrl: () => "https://vault.bitwarden.com",
|
|
getRegion: () => Region.US,
|
|
}),
|
|
} as any;
|
|
|
|
mockPlatformUtilsService = {
|
|
launchUri: jest.fn(),
|
|
} as any;
|
|
|
|
mockSubscriptionPricingService.getPersonalSubscriptionPricingTiers$.mockReturnValue(
|
|
of([mockPremiumTier, mockFamiliesTier]),
|
|
);
|
|
|
|
mockLogService = {
|
|
error: jest.fn(),
|
|
} as any;
|
|
|
|
await TestBed.configureTestingModule({
|
|
imports: [NoopAnimationsModule, PremiumUpgradeDialogComponent, CdkTrapFocus],
|
|
providers: [
|
|
{ provide: DialogRef, useValue: mockDialogRef },
|
|
{
|
|
provide: SubscriptionPricingServiceAbstraction,
|
|
useValue: mockSubscriptionPricingService,
|
|
},
|
|
{ provide: I18nService, useValue: mockI18nService },
|
|
{ provide: ToastService, useValue: mockToastService },
|
|
{ provide: EnvironmentService, useValue: mockEnvironmentService },
|
|
{ provide: PlatformUtilsService, useValue: mockPlatformUtilsService },
|
|
{ provide: LogService, useValue: mockLogService },
|
|
],
|
|
}).compileComponents();
|
|
|
|
fixture = TestBed.createComponent(PremiumUpgradeDialogComponent);
|
|
component = fixture.componentInstance;
|
|
fixture.detectChanges();
|
|
});
|
|
|
|
it("should create", () => {
|
|
expect(component).toBeTruthy();
|
|
});
|
|
|
|
it("should emit cardDetails$ observable with Premium tier data", async () => {
|
|
const cardDetails = await firstValueFrom(component["cardDetails$"]);
|
|
|
|
expect(mockSubscriptionPricingService.getPersonalSubscriptionPricingTiers$).toHaveBeenCalled();
|
|
expect(cardDetails).toBeDefined();
|
|
expect(cardDetails?.title).toBe("Premium");
|
|
});
|
|
|
|
it("should filter to Premium tier only", async () => {
|
|
const cardDetails = await firstValueFrom(component["cardDetails$"]);
|
|
|
|
expect(cardDetails?.title).toBe("Premium");
|
|
expect(cardDetails?.title).not.toBe("Families");
|
|
});
|
|
|
|
it("should map Premium tier to card details correctly", async () => {
|
|
const cardDetails = await firstValueFrom(component["cardDetails$"]);
|
|
|
|
expect(cardDetails?.title).toBe("Premium");
|
|
expect(cardDetails?.tagline).toBe("Advanced features for power users");
|
|
expect(cardDetails?.price.amount).toBe(10 / 12);
|
|
expect(cardDetails?.price.cadence).toBe("monthly");
|
|
expect(cardDetails?.button.text).toBe("upgradeNow");
|
|
expect(cardDetails?.button.type).toBe("primary");
|
|
expect(cardDetails?.features).toEqual(["Feature 1", "Feature 2", "Feature 3"]);
|
|
});
|
|
|
|
it("should use i18nService for button text", async () => {
|
|
const cardDetails = await firstValueFrom(component["cardDetails$"]);
|
|
|
|
expect(mockI18nService.t).toHaveBeenCalledWith("upgradeNow");
|
|
expect(cardDetails?.button.text).toBe("upgradeNow");
|
|
});
|
|
|
|
describe("upgrade()", () => {
|
|
it("should launch URI with query parameter", async () => {
|
|
await component["upgrade"]();
|
|
|
|
expect(mockPlatformUtilsService.launchUri).toHaveBeenCalledWith(
|
|
"https://vault.bitwarden.com/#/settings/subscription/premium?callToAction=upgradeToPremium",
|
|
);
|
|
expect(mockDialogRef.close).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
it("should close dialog when close button clicked", () => {
|
|
component["close"]();
|
|
|
|
expect(mockDialogRef.close).toHaveBeenCalled();
|
|
});
|
|
|
|
describe("error handling", () => {
|
|
it("should show error toast and return EMPTY and close dialog when getPersonalSubscriptionPricingTiers$ throws an error", (done) => {
|
|
const error = new Error("Service error");
|
|
mockSubscriptionPricingService.getPersonalSubscriptionPricingTiers$.mockReturnValue(
|
|
throwError(() => error),
|
|
);
|
|
|
|
const errorFixture = TestBed.createComponent(PremiumUpgradeDialogComponent);
|
|
const errorComponent = errorFixture.componentInstance;
|
|
errorFixture.detectChanges();
|
|
|
|
const cardDetails$ = errorComponent["cardDetails$"];
|
|
|
|
cardDetails$.subscribe({
|
|
next: () => {
|
|
done.fail("Observable should not emit any values");
|
|
},
|
|
complete: () => {
|
|
expect(mockToastService.showToast).toHaveBeenCalledWith({
|
|
variant: "error",
|
|
title: "error",
|
|
message: "unexpectedError",
|
|
});
|
|
expect(mockDialogRef.close).toHaveBeenCalled();
|
|
done();
|
|
},
|
|
error: (err: unknown) => done.fail(`Observable should not error: ${err}`),
|
|
});
|
|
});
|
|
});
|
|
});
|