mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 14:23:32 +00:00
[PM-8379] Update vault popup items service to track loading state (#9528)
This commit is contained in:
@@ -379,6 +379,54 @@ describe("VaultPopupItemsService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("loading$", () => {
|
||||||
|
let tracked: ObservableTracker<boolean>;
|
||||||
|
let trackedCiphers: ObservableTracker<any>;
|
||||||
|
beforeEach(() => {
|
||||||
|
// Start tracking loading$ emissions
|
||||||
|
tracked = new ObservableTracker(service.loading$);
|
||||||
|
|
||||||
|
// Track remainingCiphers$ to make cipher observables active
|
||||||
|
trackedCiphers = new ObservableTracker(service.remainingCiphers$);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should initialize with true first", async () => {
|
||||||
|
expect(tracked.emissions[0]).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should emit false once ciphers are available", async () => {
|
||||||
|
expect(tracked.emissions.length).toBe(2);
|
||||||
|
expect(tracked.emissions[0]).toBe(true);
|
||||||
|
expect(tracked.emissions[1]).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should cycle when cipherService.ciphers$ emits", async () => {
|
||||||
|
// Restart tracking
|
||||||
|
tracked = new ObservableTracker(service.loading$);
|
||||||
|
(cipherServiceMock.ciphers$ as BehaviorSubject<any>).next(null);
|
||||||
|
|
||||||
|
await trackedCiphers.pauseUntilReceived(2);
|
||||||
|
|
||||||
|
expect(tracked.emissions.length).toBe(3);
|
||||||
|
expect(tracked.emissions[0]).toBe(false);
|
||||||
|
expect(tracked.emissions[1]).toBe(true);
|
||||||
|
expect(tracked.emissions[2]).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should cycle when filters are applied", async () => {
|
||||||
|
// Restart tracking
|
||||||
|
tracked = new ObservableTracker(service.loading$);
|
||||||
|
service.applyFilter("test");
|
||||||
|
|
||||||
|
await trackedCiphers.pauseUntilReceived(2);
|
||||||
|
|
||||||
|
expect(tracked.emissions.length).toBe(3);
|
||||||
|
expect(tracked.emissions[0]).toBe(false);
|
||||||
|
expect(tracked.emissions[1]).toBe(true);
|
||||||
|
expect(tracked.emissions[2]).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("applyFilter", () => {
|
describe("applyFilter", () => {
|
||||||
it("should call search Service with the new search term", (done) => {
|
it("should call search Service with the new search term", (done) => {
|
||||||
const searchText = "Hello";
|
const searchText = "Hello";
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { inject, Injectable, NgZone } from "@angular/core";
|
|||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
combineLatest,
|
combineLatest,
|
||||||
|
distinctUntilChanged,
|
||||||
distinctUntilKeyChanged,
|
distinctUntilKeyChanged,
|
||||||
from,
|
from,
|
||||||
map,
|
map,
|
||||||
@@ -12,6 +13,8 @@ import {
|
|||||||
startWith,
|
startWith,
|
||||||
Subject,
|
Subject,
|
||||||
switchMap,
|
switchMap,
|
||||||
|
tap,
|
||||||
|
withLatestFrom,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
|
|
||||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
@@ -40,6 +43,13 @@ import { MY_VAULT_ID, VaultPopupListFiltersService } from "./vault-popup-list-fi
|
|||||||
export class VaultPopupItemsService {
|
export class VaultPopupItemsService {
|
||||||
private _refreshCurrentTab$ = new Subject<void>();
|
private _refreshCurrentTab$ = new Subject<void>();
|
||||||
private _searchText$ = new BehaviorSubject<string>("");
|
private _searchText$ = new BehaviorSubject<string>("");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subject that emits whenever new ciphers are being processed/filtered.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private _ciphersLoading$ = new Subject<void>();
|
||||||
|
|
||||||
latestSearchText$: Observable<string> = this._searchText$.asObservable();
|
latestSearchText$: Observable<string> = this._searchText$.asObservable();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -84,6 +94,7 @@ export class VaultPopupItemsService {
|
|||||||
this.cipherService.localData$,
|
this.cipherService.localData$,
|
||||||
).pipe(
|
).pipe(
|
||||||
runInsideAngular(inject(NgZone)), // Workaround to ensure cipher$ state provider emissions are run inside Angular
|
runInsideAngular(inject(NgZone)), // Workaround to ensure cipher$ state provider emissions are run inside Angular
|
||||||
|
tap(() => this._ciphersLoading$.next()),
|
||||||
switchMap(() => Utils.asyncToObservable(() => this.cipherService.getAllDecrypted())),
|
switchMap(() => Utils.asyncToObservable(() => this.cipherService.getAllDecrypted())),
|
||||||
switchMap((ciphers) =>
|
switchMap((ciphers) =>
|
||||||
combineLatest([
|
combineLatest([
|
||||||
@@ -112,6 +123,7 @@ export class VaultPopupItemsService {
|
|||||||
this._searchText$,
|
this._searchText$,
|
||||||
this.vaultPopupListFiltersService.filterFunction$,
|
this.vaultPopupListFiltersService.filterFunction$,
|
||||||
]).pipe(
|
]).pipe(
|
||||||
|
tap(() => this._ciphersLoading$.next()),
|
||||||
map(([ciphers, searchText, filterFunction]): [CipherView[], string] => [
|
map(([ciphers, searchText, filterFunction]): [CipherView[], string] => [
|
||||||
filterFunction(ciphers),
|
filterFunction(ciphers),
|
||||||
searchText,
|
searchText,
|
||||||
@@ -148,10 +160,8 @@ export class VaultPopupItemsService {
|
|||||||
* List of favorite ciphers that are not currently suggested for autofill.
|
* List of favorite ciphers that are not currently suggested for autofill.
|
||||||
* Ciphers are sorted by last used date, then by name.
|
* Ciphers are sorted by last used date, then by name.
|
||||||
*/
|
*/
|
||||||
favoriteCiphers$: Observable<PopupCipherView[]> = combineLatest([
|
favoriteCiphers$: Observable<PopupCipherView[]> = this.autoFillCiphers$.pipe(
|
||||||
this.autoFillCiphers$,
|
withLatestFrom(this._filteredCipherList$),
|
||||||
this._filteredCipherList$,
|
|
||||||
]).pipe(
|
|
||||||
map(([autoFillCiphers, ciphers]) =>
|
map(([autoFillCiphers, ciphers]) =>
|
||||||
ciphers.filter((cipher) => cipher.favorite && !autoFillCiphers.includes(cipher)),
|
ciphers.filter((cipher) => cipher.favorite && !autoFillCiphers.includes(cipher)),
|
||||||
),
|
),
|
||||||
@@ -165,12 +175,9 @@ export class VaultPopupItemsService {
|
|||||||
* List of all remaining ciphers that are not currently suggested for autofill or marked as favorite.
|
* List of all remaining ciphers that are not currently suggested for autofill or marked as favorite.
|
||||||
* Ciphers are sorted by name.
|
* Ciphers are sorted by name.
|
||||||
*/
|
*/
|
||||||
remainingCiphers$: Observable<PopupCipherView[]> = combineLatest([
|
remainingCiphers$: Observable<PopupCipherView[]> = this.favoriteCiphers$.pipe(
|
||||||
this.autoFillCiphers$,
|
withLatestFrom(this._filteredCipherList$, this.autoFillCiphers$),
|
||||||
this.favoriteCiphers$,
|
map(([favoriteCiphers, ciphers, autoFillCiphers]) =>
|
||||||
this._filteredCipherList$,
|
|
||||||
]).pipe(
|
|
||||||
map(([autoFillCiphers, favoriteCiphers, ciphers]) =>
|
|
||||||
ciphers.filter(
|
ciphers.filter(
|
||||||
(cipher) => !autoFillCiphers.includes(cipher) && !favoriteCiphers.includes(cipher),
|
(cipher) => !autoFillCiphers.includes(cipher) && !favoriteCiphers.includes(cipher),
|
||||||
),
|
),
|
||||||
@@ -179,6 +186,14 @@ export class VaultPopupItemsService {
|
|||||||
shareReplay({ refCount: false, bufferSize: 1 }),
|
shareReplay({ refCount: false, bufferSize: 1 }),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable that indicates whether the service is currently loading ciphers.
|
||||||
|
*/
|
||||||
|
loading$: Observable<boolean> = merge(
|
||||||
|
this._ciphersLoading$.pipe(map(() => true)),
|
||||||
|
this.remainingCiphers$.pipe(map(() => false)),
|
||||||
|
).pipe(startWith(true), distinctUntilChanged(), shareReplay({ refCount: false, bufferSize: 1 }));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observable that indicates whether a filter is currently applied to the ciphers.
|
* Observable that indicates whether a filter is currently applied to the ciphers.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Observable, Subject, Subscription, firstValueFrom, throwError, timeout } from "rxjs";
|
import { firstValueFrom, Observable, Subject, Subscription, throwError, timeout } from "rxjs";
|
||||||
|
|
||||||
/** Test class to enable async awaiting of observable emissions */
|
/** Test class to enable async awaiting of observable emissions */
|
||||||
export class ObservableTracker<T> {
|
export class ObservableTracker<T> {
|
||||||
@@ -43,6 +43,9 @@ export class ObservableTracker<T> {
|
|||||||
|
|
||||||
private trackEmissions(observable: Observable<T>): T[] {
|
private trackEmissions(observable: Observable<T>): T[] {
|
||||||
const emissions: T[] = [];
|
const emissions: T[] = [];
|
||||||
|
this.emissionReceived.subscribe((value) => {
|
||||||
|
emissions.push(value);
|
||||||
|
});
|
||||||
this.subscription = observable.subscribe((value) => {
|
this.subscription = observable.subscribe((value) => {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
this.emissionReceived.next(null);
|
this.emissionReceived.next(null);
|
||||||
@@ -64,9 +67,7 @@ export class ObservableTracker<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.emissionReceived.subscribe((value) => {
|
|
||||||
emissions.push(value);
|
|
||||||
});
|
|
||||||
return emissions;
|
return emissions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user