add tenant and user endpoints, services, models and guards

This commit is contained in:
Gagan7900
2026-07-18 16:38:41 +05:30
parent be86ab3564
commit 7c4e34e478
50 changed files with 3064 additions and 451 deletions
@@ -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');