1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-26 09:33:22 +00:00

only show delete button when the user has delete permissions (#19200)

This commit is contained in:
Nick Krantz
2026-02-24 10:36:29 -06:00
committed by GitHub
parent 3e4fef63a0
commit c03ea5b29f
3 changed files with 192 additions and 13 deletions

View File

@@ -63,14 +63,21 @@
</button>
}
<button
type="button"
(click)="delete()"
class="danger"
appA11yTitle="{{ (cipher.isDeleted ? 'permanentlyDelete' : 'delete') | i18n }}"
>
<i class="bwi bwi-trash bwi-lg bwi-fw" aria-hidden="true" style="pointer-events: none"></i>
</button>
@if (canDelete) {
<button
type="button"
(click)="delete()"
class="danger"
appA11yTitle="{{ (cipher.isDeleted ? 'permanentlyDelete' : 'delete') | i18n }}"
data-test-id="footer-delete-button"
>
<i
class="bwi bwi-trash bwi-lg bwi-fw"
aria-hidden="true"
style="pointer-events: none"
></i>
</button>
}
</div>
}
</div>

View File

@@ -0,0 +1,172 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { By } from "@angular/platform-browser";
import { mock } from "jest-mock-extended";
import { of } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { ButtonModule, DialogService, ToastService } from "@bitwarden/components";
import { ArchiveCipherUtilitiesService, PasswordRepromptService } from "@bitwarden/vault";
import { ItemFooterComponent } from "./item-footer.component";
describe("ItemFooterComponent", () => {
let component: ItemFooterComponent;
let fixture: ComponentFixture<ItemFooterComponent>;
let accountService: FakeAccountService;
const mockUserId = Utils.newGuid() as UserId;
beforeEach(async () => {
accountService = mockAccountServiceWith(mockUserId);
const cipherArchiveService = {
userCanArchive$: jest.fn().mockReturnValue(of(false)),
hasArchiveFlagEnabled$: of(false),
};
await TestBed.configureTestingModule({
imports: [ItemFooterComponent, ButtonModule, JslibModule],
providers: [
{ provide: AccountService, useValue: accountService },
{ provide: CipherService, useValue: mock<CipherService>() },
{ provide: DialogService, useValue: mock<DialogService>() },
{ provide: PasswordRepromptService, useValue: mock<PasswordRepromptService>() },
{ provide: CipherAuthorizationService, useValue: mock<CipherAuthorizationService>() },
{ provide: ToastService, useValue: mock<ToastService>() },
{ provide: I18nService, useValue: { t: (key: string) => key } },
{ provide: LogService, useValue: mock<LogService>() },
{ provide: CipherArchiveService, useValue: cipherArchiveService },
{ provide: ArchiveCipherUtilitiesService, useValue: mock<ArchiveCipherUtilitiesService>() },
],
}).compileComponents();
fixture = TestBed.createComponent(ItemFooterComponent);
component = fixture.componentInstance;
});
const createCipherView = (overrides: Partial<CipherView> = {}): CipherView => {
const cipher = new CipherView();
cipher.id = "test-cipher-id";
cipher.permissions = {
delete: false,
restore: false,
manage: false,
edit: false,
view: false,
viewPassword: false,
};
return Object.assign(cipher, overrides);
};
describe("delete button visibility", () => {
it("shows the delete button when cipher.permissions.delete is true and action is 'view'", async () => {
const cipher = createCipherView({
permissions: {
delete: true,
restore: false,
manage: false,
edit: false,
view: false,
viewPassword: false,
},
});
component.cipher = cipher;
component.action = "view";
await component.ngOnInit();
fixture.detectChanges();
const deleteButton = fixture.debugElement.query(
By.css('[data-test-id="footer-delete-button"]'),
);
expect(deleteButton).toBeTruthy();
});
it("shows the delete button when cipher.permissions.delete is true and action is 'edit'", async () => {
const cipher = createCipherView({
permissions: {
delete: true,
restore: false,
manage: false,
edit: false,
view: false,
viewPassword: false,
},
});
component.cipher = cipher;
component.action = "edit";
await component.ngOnInit();
fixture.detectChanges();
const deleteButton = fixture.debugElement.query(
By.css('[data-test-id="footer-delete-button"]'),
);
expect(deleteButton).toBeTruthy();
});
it("does not show the delete button when cipher.permissions.delete is false", async () => {
const cipher = createCipherView({
permissions: {
delete: false,
restore: false,
manage: false,
edit: false,
view: false,
viewPassword: false,
},
});
component.cipher = cipher;
component.action = "view";
await component.ngOnInit();
fixture.detectChanges();
const deleteButton = fixture.debugElement.query(
By.css('[data-test-id="footer-delete-button"]'),
);
expect(deleteButton).toBeFalsy();
});
it("does not show the delete button when action is not 'view' or 'edit'", async () => {
const cipher = createCipherView({
permissions: {
delete: true,
restore: false,
manage: false,
edit: false,
view: false,
viewPassword: false,
},
});
component.cipher = cipher;
component.action = "add";
await component.ngOnInit();
fixture.detectChanges();
const deleteButton = fixture.debugElement.query(
By.css('[data-test-id="footer-delete-button"]'),
);
expect(deleteButton).toBeFalsy();
});
});
});

View File

@@ -128,11 +128,7 @@ export class ItemFooterComponent implements OnInit, OnChanges {
}
protected get hasFooterAction() {
return (
this.showArchiveButton ||
this.showUnarchiveButton ||
(this.cipher.permissions?.delete && (this.action === "edit" || this.action === "view"))
);
return this.showArchiveButton || this.showUnarchiveButton || this.canDelete;
}
protected get showCloneOption() {
@@ -145,6 +141,10 @@ export class ItemFooterComponent implements OnInit, OnChanges {
);
}
protected get canDelete() {
return this.cipher.permissions?.delete && (this.action === "edit" || this.action === "view");
}
cancel() {
this.onCancel.emit(this.cipher);
}