add tenant and user endpoints, services, models and guards
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
import { OverlayContainer } from '@angular/cdk/overlay';
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { Observable, Subject, of, throwError } from 'rxjs';
|
||||
|
||||
import { Autocomplete } from './autocomplete';
|
||||
import { AutocompleteSearchFn } from './autocomplete.types';
|
||||
import { AutocompleteResolveValueFn, AutocompleteSearchFn } from './autocomplete.types';
|
||||
|
||||
interface LookupItem {
|
||||
readonly code: string;
|
||||
@@ -26,24 +26,29 @@ const INDONESIA: LookupItem = { code: 'ID', title: 'Indonesia' };
|
||||
[searchFn]="searchFn"
|
||||
[displayWith]="displayWith"
|
||||
[valueWith]="valueWith"
|
||||
placeholder="e.g.: USD"
|
||||
[selectedItem]="selectedItem()"
|
||||
[resolveValueFn]="resolveValueFn()"
|
||||
[minSearchLength]="minLength"
|
||||
[debounceTime]="delay()"
|
||||
[readonly]="readonly()"
|
||||
[showDropdownOnFocus]="openOnFocus"
|
||||
[showDropdownOnFocus]="openOnFocus()"
|
||||
[submitAttempted]="submitAttempted()"
|
||||
/>
|
||||
`
|
||||
})
|
||||
class HostComponent {
|
||||
readonly control = new FormControl<string | null>(null);
|
||||
readonly selectedItem = signal<LookupItem | null>(null);
|
||||
readonly resolveValueFn = signal<AutocompleteResolveValueFn<LookupItem, string> | null>(null);
|
||||
searchFn: AutocompleteSearchFn<LookupItem> = () => of([INDIA, INDONESIA]);
|
||||
readonly displayWith = (item: LookupItem): string => item.title;
|
||||
readonly valueWith = (item: LookupItem): string => item.code;
|
||||
minLength = 2;
|
||||
readonly delay = signal(300);
|
||||
readonly readonly = signal(false);
|
||||
openOnFocus = false;
|
||||
readonly openOnFocus = signal(false);
|
||||
readonly submitAttempted = signal(false);
|
||||
}
|
||||
|
||||
describe('Autocomplete', () => {
|
||||
@@ -73,6 +78,11 @@ describe('Autocomplete', () => {
|
||||
it('initializes with an empty selection', () => {
|
||||
expect(component.searchText()).toBe('');
|
||||
expect(component.options()).toEqual([]);
|
||||
expect(input().value).toBe('');
|
||||
expect(input().placeholder).toBe('e.g.: USD');
|
||||
expect(input().className).not.toContain('ti-form-control');
|
||||
expect(input().className).toContain('placeholder:text-textmuted');
|
||||
expect(input().className).toContain('dark:placeholder:text-white/50');
|
||||
});
|
||||
|
||||
it('integrates with a reactive form control', () => {
|
||||
@@ -80,6 +90,14 @@ describe('Autocomplete', () => {
|
||||
expect(host.control.value).toBe('IN');
|
||||
});
|
||||
|
||||
it('treats edited selected text as search text rather than a selected item', () => {
|
||||
component.select(INDIA);
|
||||
expect(component.activeItem()).toEqual(INDIA);
|
||||
type('Ind');
|
||||
expect(component.searchText()).toBe('Ind');
|
||||
expect(component.activeItem()).toBeNull();
|
||||
});
|
||||
|
||||
it('does not emit onChange from writeValue', () => {
|
||||
const change = vi.fn();
|
||||
component.registerOnChange(change);
|
||||
@@ -87,6 +105,28 @@ describe('Autocomplete', () => {
|
||||
expect(change).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not show invalid styling after touch before submit', () => {
|
||||
host.control.setValidators(Validators.required);
|
||||
host.control.updateValueAndValidity();
|
||||
host.control.markAsTouched();
|
||||
fixture.detectChanges();
|
||||
expect(input().classList.contains('is-invalid')).toBe(false);
|
||||
});
|
||||
|
||||
it('shows invalid styling after submit and removes it when valid', () => {
|
||||
host.control.setValidators(Validators.required);
|
||||
host.control.updateValueAndValidity();
|
||||
host.submitAttempted.set(true);
|
||||
fixture.detectChanges();
|
||||
expect(input().classList.contains('is-invalid')).toBe(true);
|
||||
expect(fixture.nativeElement.textContent).toContain('Country is required.');
|
||||
|
||||
host.control.setValue('IN');
|
||||
fixture.detectChanges();
|
||||
expect(input().classList.contains('is-invalid')).toBe(false);
|
||||
expect(fixture.nativeElement.textContent).not.toContain('Country is required.');
|
||||
});
|
||||
|
||||
it('applies a disabled form state', () => {
|
||||
host.control.disable();
|
||||
fixture.detectChanges();
|
||||
@@ -117,6 +157,40 @@ describe('Autocomplete', () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('loads five preview records on focus before the user types', () => {
|
||||
vi.useFakeTimers();
|
||||
const search = vi.fn(() => of([INDIA, INDONESIA]));
|
||||
host.openOnFocus.set(true);
|
||||
host.searchFn = search;
|
||||
fixture.detectChanges();
|
||||
input().focus();
|
||||
vi.advanceTimersByTime(0);
|
||||
fixture.detectChanges();
|
||||
expect(search).toHaveBeenCalledWith('', 5);
|
||||
expect(component.options()).toEqual([INDIA, INDONESIA]);
|
||||
expect(component.showingPreview()).toBe(true);
|
||||
expect(overlayContainer.getContainerElement().textContent).toContain('Type to search more...');
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('replaces preview records with typed search results', () => {
|
||||
vi.useFakeTimers();
|
||||
host.openOnFocus.set(true);
|
||||
host.delay.set(0);
|
||||
host.searchFn = (term, _limit) => of(term ? [INDONESIA] : [INDIA]);
|
||||
fixture.detectChanges();
|
||||
input().focus();
|
||||
vi.advanceTimersByTime(0);
|
||||
expect(component.options()).toEqual([INDIA]);
|
||||
type('in');
|
||||
vi.advanceTimersByTime(0);
|
||||
fixture.detectChanges();
|
||||
expect(component.options()).toEqual([INDONESIA]);
|
||||
expect(component.showingPreview()).toBe(false);
|
||||
expect(overlayContainer.getContainerElement().textContent).not.toContain('Type to search more...');
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('cancels stale search requests', () => {
|
||||
vi.useFakeTimers();
|
||||
const first = new Subject<readonly LookupItem[]>();
|
||||
@@ -227,6 +301,36 @@ describe('Autocomplete', () => {
|
||||
expect(component.searchText()).toBe('India');
|
||||
});
|
||||
|
||||
it('does not resolve an empty string form value', () => {
|
||||
const resolve = vi.fn(() => of(INDIA));
|
||||
host.resolveValueFn.set(resolve);
|
||||
fixture.detectChanges();
|
||||
host.control.setValue('');
|
||||
fixture.detectChanges();
|
||||
expect(resolve).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses the matching selected item without resolving it again', () => {
|
||||
const resolve = vi.fn(() => of(INDIA));
|
||||
host.resolveValueFn.set(resolve);
|
||||
host.selectedItem.set(INDIA);
|
||||
fixture.detectChanges();
|
||||
host.control.setValue('IN');
|
||||
fixture.detectChanges();
|
||||
expect(component.searchText()).toBe('India');
|
||||
expect(resolve).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('resolves a non-empty value when no matching selected item is supplied', () => {
|
||||
const resolve = vi.fn(() => of(INDIA));
|
||||
host.resolveValueFn.set(resolve);
|
||||
fixture.detectChanges();
|
||||
host.control.setValue('IN');
|
||||
fixture.detectChanges();
|
||||
expect(resolve).toHaveBeenCalledWith('IN');
|
||||
expect(component.searchText()).toBe('India');
|
||||
});
|
||||
|
||||
it('does not retain a stale label when edit values change', () => {
|
||||
host.selectedItem.set(INDIA);
|
||||
host.control.setValue('IN');
|
||||
|
||||
Reference in New Issue
Block a user