1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-07 04:03:29 +00:00

Merge branch 'main' into beeep/dev-container

This commit is contained in:
Conner Turnbull
2026-02-04 13:19:47 -05:00
committed by GitHub
9 changed files with 132 additions and 17 deletions

View File

@@ -74,9 +74,11 @@
<button type="button" bitMenuItem (click)="edit(cipher)">
{{ "edit" | i18n }}
</button>
<button type="button" bitMenuItem (click)="clone(cipher)">
{{ "clone" | i18n }}
</button>
@if (userHasPremium$ | async) {
<button type="button" bitMenuItem (click)="clone(cipher)">
{{ "clone" | i18n }}
</button>
}
@if (canAssignCollections$ | async) {
<button
type="button"

View File

@@ -1,4 +1,6 @@
import { TestBed } from "@angular/core/testing";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { By } from "@angular/platform-browser";
import { provideNoopAnimations } from "@angular/platform-browser/animations";
import { Router } from "@angular/router";
import { mock } from "jest-mock-extended";
import { BehaviorSubject, of } from "rxjs";
@@ -7,10 +9,13 @@ import { CollectionService } from "@bitwarden/admin-console/common";
import { PopupRouterCacheService } from "@bitwarden/browser/platform/popup/view-cache/popup-router-cache.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { EnvironmentService } 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 { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
import { DialogService, ToastService } from "@bitwarden/components";
import { LogService } from "@bitwarden/logging";
@@ -25,40 +30,78 @@ jest.mock("qrcode-parser", () => {});
describe("ArchiveComponent", () => {
let component: ArchiveComponent;
let fixture: ComponentFixture<ArchiveComponent>;
let hasOrganizations: jest.Mock;
let decryptedCollections$: jest.Mock;
let navigate: jest.Mock;
let showPasswordPrompt: jest.Mock;
let userHasPremium$: jest.Mock;
let archivedCiphers$: jest.Mock;
beforeAll(async () => {
beforeEach(async () => {
navigate = jest.fn();
showPasswordPrompt = jest.fn().mockResolvedValue(true);
hasOrganizations = jest.fn();
decryptedCollections$ = jest.fn();
hasOrganizations = jest.fn().mockReturnValue(of(false));
decryptedCollections$ = jest.fn().mockReturnValue(of([]));
userHasPremium$ = jest.fn().mockReturnValue(of(false));
archivedCiphers$ = jest.fn().mockReturnValue(of([{ id: "cipher-1" }]));
await TestBed.configureTestingModule({
imports: [ArchiveComponent],
providers: [
provideNoopAnimations(),
{ provide: Router, useValue: { navigate } },
{
provide: AccountService,
useValue: { activeAccount$: new BehaviorSubject({ id: "user-id" }) },
},
{ provide: PasswordRepromptService, useValue: { showPasswordPrompt } },
{ provide: OrganizationService, useValue: { hasOrganizations } },
{
provide: OrganizationService,
useValue: { hasOrganizations, organizations$: () => of([]) },
},
{ provide: CollectionService, useValue: { decryptedCollections$ } },
{ provide: DialogService, useValue: mock<DialogService>() },
{ provide: CipherService, useValue: mock<CipherService>() },
{ provide: CipherArchiveService, useValue: mock<CipherArchiveService>() },
{
provide: CipherArchiveService,
useValue: {
userHasPremium$,
archivedCiphers$,
userCanArchive$: jest.fn().mockReturnValue(of(true)),
showSubscriptionEndedMessaging$: jest.fn().mockReturnValue(of(false)),
},
},
{ provide: ToastService, useValue: mock<ToastService>() },
{ provide: PopupRouterCacheService, useValue: mock<PopupRouterCacheService>() },
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
{ provide: LogService, useValue: mock<LogService>() },
{ provide: I18nService, useValue: { t: (key: string) => key } },
{
provide: EnvironmentService,
useValue: {
environment$: of({
getIconsUrl: () => "https://icons.example.com",
}),
},
},
{
provide: DomainSettingsService,
useValue: {
showFavicons$: of(true),
},
},
{
provide: CipherAuthorizationService,
useValue: {
canDeleteCipher$: jest.fn().mockReturnValue(of(true)),
},
},
],
}).compileComponents();
const fixture = TestBed.createComponent(ArchiveComponent);
fixture = TestBed.createComponent(ArchiveComponent);
component = fixture.componentInstance;
});
@@ -137,4 +180,54 @@ describe("ArchiveComponent", () => {
expect(navigate).not.toHaveBeenCalled();
});
});
describe("clone menu option", () => {
const getBitMenuPanel = () => document.querySelector(".bit-menu-panel");
it("is shown when user has premium", async () => {
userHasPremium$.mockReturnValue(of(true));
const testFixture = TestBed.createComponent(ArchiveComponent);
testFixture.detectChanges();
await testFixture.whenStable();
const menuTrigger = testFixture.debugElement.query(By.css('button[aria-haspopup="menu"]'));
expect(menuTrigger).toBeTruthy();
(menuTrigger.nativeElement as HTMLButtonElement).click();
testFixture.detectChanges();
const menuPanel = getBitMenuPanel();
expect(menuPanel).toBeTruthy();
const menuButtons = menuPanel?.querySelectorAll("button[bitMenuItem]");
const cloneButtonFound = Array.from(menuButtons || []).some(
(btn) => btn.textContent?.trim() === "clone",
);
expect(cloneButtonFound).toBe(true);
});
it("is not shown when user does not have premium", async () => {
userHasPremium$.mockReturnValue(of(false));
const testFixture = TestBed.createComponent(ArchiveComponent);
testFixture.detectChanges();
await testFixture.whenStable();
const menuTrigger = testFixture.debugElement.query(By.css('button[aria-haspopup="menu"]'));
expect(menuTrigger).toBeTruthy();
(menuTrigger.nativeElement as HTMLButtonElement).click();
testFixture.detectChanges();
const menuPanel = getBitMenuPanel();
expect(menuPanel).toBeTruthy();
const menuButtons = menuPanel?.querySelectorAll("button[bitMenuItem]");
const cloneButtonFound = Array.from(menuButtons || []).some(
(btn) => btn.textContent?.trim() === "clone",
);
expect(cloneButtonFound).toBe(false);
});
});
});

View File

@@ -135,6 +135,10 @@ export class ArchiveComponent {
switchMap((userId) => this.cipherArchiveService.showSubscriptionEndedMessaging$(userId)),
);
protected userHasPremium$ = this.userId$.pipe(
switchMap((userId) => this.cipherArchiveService.userHasPremium$(userId)),
);
async navigateToPremium() {
await this.router.navigate(["/premium"]);
}

View File

@@ -69,7 +69,7 @@
"browser-hrtime": "1.1.8",
"chalk": "4.1.2",
"commander": "14.0.0",
"core-js": "3.47.0",
"core-js": "3.48.0",
"form-data": "4.0.4",
"https-proxy-agent": "7.0.6",
"inquirer": "8.2.6",

View File

@@ -38,6 +38,9 @@
"accessIntelligence": {
"message": "Access Intelligence"
},
"noApplicationsMatchTheseFilters": {
"message": "No applications match these filters"
},
"passwordRisk": {
"message": "Password Risk"
},

View File

@@ -43,5 +43,11 @@
[checkboxChange]="onCheckboxChange"
[showAppAtRiskMembers]="showAppAtRiskMembers"
></app-table-row-scrollable-m11>
@if (emptyTableExplanation()) {
<div class="tw-flex tw-mt-10 tw-justify-center">
<span bitTypography="body2">{{ emptyTableExplanation() }}</span>
</div>
}
</div>
}

View File

@@ -104,6 +104,7 @@ export class ApplicationsComponent implements OnInit {
icon: " ",
},
]);
protected readonly emptyTableExplanation = signal("");
constructor(
protected i18nService: I18nService,
@@ -164,6 +165,12 @@ export class ApplicationsComponent implements OnInit {
this.dataSource.filter = (app) =>
filterFunction(app) &&
app.applicationName.toLowerCase().includes(searchText.toLowerCase());
if (this.dataSource?.filteredData?.length === 0) {
this.emptyTableExplanation.set(this.i18nService.t("noApplicationsMatchTheseFilters"));
} else {
this.emptyTableExplanation.set("");
}
});
}

10
package-lock.json generated
View File

@@ -38,7 +38,7 @@
"bufferutil": "4.1.0",
"chalk": "4.1.2",
"commander": "14.0.0",
"core-js": "3.47.0",
"core-js": "3.48.0",
"form-data": "4.0.4",
"https-proxy-agent": "7.0.6",
"inquirer": "8.2.6",
@@ -205,7 +205,7 @@
"browser-hrtime": "1.1.8",
"chalk": "4.1.2",
"commander": "14.0.0",
"core-js": "3.47.0",
"core-js": "3.48.0",
"form-data": "4.0.4",
"https-proxy-agent": "7.0.6",
"inquirer": "8.2.6",
@@ -20879,9 +20879,9 @@
}
},
"node_modules/core-js": {
"version": "3.47.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz",
"integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==",
"version": "3.48.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz",
"integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==",
"hasInstallScript": true,
"license": "MIT",
"funding": {

View File

@@ -177,7 +177,7 @@
"bufferutil": "4.1.0",
"chalk": "4.1.2",
"commander": "14.0.0",
"core-js": "3.47.0",
"core-js": "3.48.0",
"form-data": "4.0.4",
"https-proxy-agent": "7.0.6",
"inquirer": "8.2.6",