fix: stabilize admin user UI state
This commit is contained in:
@@ -94,7 +94,7 @@ describe('Users directory', () => {
|
||||
http = TestBed.inject(HttpTestingController);
|
||||
});
|
||||
|
||||
afterEach(() => http.verify());
|
||||
afterEach(() => http.verify({ ignoreCancelled: true }));
|
||||
|
||||
function create(): void {
|
||||
fixture = TestBed.createComponent(Users);
|
||||
@@ -113,6 +113,13 @@ describe('Users directory', () => {
|
||||
return match as HTMLButtonElement;
|
||||
}
|
||||
|
||||
|
||||
function childButton(host: HTMLElement, label: string): HTMLButtonElement {
|
||||
const match = [...host.querySelectorAll('button')].find((element) => element.textContent?.includes(label));
|
||||
if (!match) throw new Error(`Missing child button: ${label}`);
|
||||
return match as HTMLButtonElement;
|
||||
}
|
||||
|
||||
function flushDirectory(page = directoryPage()): void {
|
||||
http.expectOne(`${api}?page=1&limit=20`).flush(page);
|
||||
fixture.detectChanges();
|
||||
@@ -147,6 +154,25 @@ describe('Users directory', () => {
|
||||
http.expectOne(`${api}?page=2&limit=20`).flush(directoryPage([admin], 2, 12, false));
|
||||
});
|
||||
|
||||
it('cancels an older directory load when a newer search starts', () => {
|
||||
create();
|
||||
const older = http.expectOne(`${api}?page=1&limit=20`);
|
||||
const search = (fixture.nativeElement as HTMLElement).querySelector<HTMLInputElement>('input[type="search"]')!;
|
||||
search.value = 'Grace';
|
||||
search.dispatchEvent(new Event('input'));
|
||||
search.closest('form')!.dispatchEvent(new Event('submit'));
|
||||
const newer = http.expectOne(`${api}?page=1&limit=20&search=Grace`);
|
||||
|
||||
const olderWasCancelled = older.cancelled;
|
||||
newer.flush(directoryPage([admin]));
|
||||
if (!olderWasCancelled) older.flush(directoryPage([ada]));
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(olderWasCancelled).toBe(true);
|
||||
expect(text()).toContain('Grace Admin');
|
||||
expect(text()).not.toContain('Ada Lovelace');
|
||||
});
|
||||
|
||||
it('redacts admin-only data and controls for a non-admin even if extra fields arrive', () => {
|
||||
create();
|
||||
flushDirectory(directoryPage([ada]));
|
||||
@@ -208,6 +234,31 @@ describe('Users directory', () => {
|
||||
expect(text()).toContain('Augusta King');
|
||||
});
|
||||
|
||||
it('keeps the editor draft while saving input changes and on an initial profile failure', () => {
|
||||
isAdmin.set(true);
|
||||
create();
|
||||
flushDirectory();
|
||||
button('Bearbeiten').click();
|
||||
fixture.detectChanges();
|
||||
const firstName = (fixture.nativeElement as HTMLElement).querySelector<HTMLInputElement>('input[name="firstName"]')!;
|
||||
firstName.value = 'Operator draft';
|
||||
firstName.dispatchEvent(new Event('input'));
|
||||
firstName.closest('form')!.dispatchEvent(new Event('submit'));
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(firstName.value).toBe('Operator draft');
|
||||
http.expectOne(`${adminApi}/7/profile`).flush(
|
||||
{ message: 'Profile rejected' },
|
||||
{ status: 400, statusText: 'Bad Request' },
|
||||
);
|
||||
http.expectNone(`${api}?page=1&limit=20`);
|
||||
fixture.detectChanges();
|
||||
expect((fixture.nativeElement as HTMLElement).querySelector<HTMLInputElement>('input[name="firstName"]')!.value).toBe(
|
||||
'Operator draft',
|
||||
);
|
||||
expect(text()).toContain('Profile rejected');
|
||||
});
|
||||
|
||||
it('reloads authoritative directory state when a role change fails after the profile was saved', () => {
|
||||
isAdmin.set(true);
|
||||
create();
|
||||
@@ -241,11 +292,69 @@ describe('Users directory', () => {
|
||||
closeDialog.next(true);
|
||||
const request = http.expectOne(`${adminApi}/7/status`);
|
||||
expect(request.request.body).toEqual({ status: 2 });
|
||||
fixture.detectChanges();
|
||||
expect(text()).toContain('Status wird geändert');
|
||||
expect((fixture.nativeElement as HTMLElement).querySelector('[data-user-id="7"]')?.getAttribute('aria-busy')).toBe('true');
|
||||
request.flush(ada);
|
||||
http.expectOne(`${api}?page=1&limit=20`).flush(directoryPage([{ ...ada, status: { id: 2, name: 'Inactive' } }]));
|
||||
});
|
||||
|
||||
it('loads searchable player results and assigns an unlinked player before refreshing both lists', () => {
|
||||
it('searches players from page one after paging and assigns before refreshing both lists', () => {
|
||||
isAdmin.set(true);
|
||||
create();
|
||||
flushDirectory();
|
||||
button('Zuordnungen verwalten').click();
|
||||
fixture.detectChanges();
|
||||
const panel = (fixture.nativeElement as HTMLElement).querySelector<HTMLElement>('app-player-assignments')!;
|
||||
expect(panel.textContent).toContain('Spieler werden geladen');
|
||||
http.expectOne(`${adminApi}/players?assignment=all&page=1&limit=20`).flush(playersPage({ hasNextPage: true, total: 21 }));
|
||||
fixture.detectChanges();
|
||||
expect(panel.querySelector('nav[aria-label="Spielerseiten"]')).not.toBeNull();
|
||||
childButton(panel, 'Weiter').click();
|
||||
http.expectOne(`${adminApi}/players?assignment=all&page=2&limit=20`).flush(playersPage({ page: 2 }));
|
||||
fixture.detectChanges();
|
||||
const playerSearch = panel.querySelector<HTMLInputElement>('input[type="search"]')!;
|
||||
playerSearch.value = 'Linus';
|
||||
playerSearch.dispatchEvent(new Event('input'));
|
||||
playerSearch.closest('form')!.dispatchEvent(new Event('submit'));
|
||||
http.expectOne(`${adminApi}/players?search=Linus&assignment=all&page=1&limit=20`).flush(playersPage());
|
||||
fixture.detectChanges();
|
||||
expect(text()).toContain('Linus Player');
|
||||
expect(text()).toContain('Second Team');
|
||||
|
||||
childButton(panel, 'Zuordnen').click();
|
||||
const assign = http.expectOne(`${adminApi}/7/players/202`);
|
||||
expect(assign.request.method).toBe('PUT');
|
||||
assign.flush(ada);
|
||||
http.expectOne(`${api}?page=1&limit=20`).flush(directoryPage());
|
||||
http.expectOne(`${adminApi}/players?search=Linus&assignment=all&page=1&limit=20`).flush(playersPage());
|
||||
});
|
||||
|
||||
it('cancels an older player load when a newer player search starts', () => {
|
||||
isAdmin.set(true);
|
||||
create();
|
||||
flushDirectory();
|
||||
button('Zuordnungen verwalten').click();
|
||||
fixture.detectChanges();
|
||||
const older = http.expectOne(`${adminApi}/players?assignment=all&page=1&limit=20`);
|
||||
const panel = (fixture.nativeElement as HTMLElement).querySelector<HTMLElement>('app-player-assignments')!;
|
||||
const playerSearch = panel.querySelector<HTMLInputElement>('input[type="search"]')!;
|
||||
playerSearch.value = 'New';
|
||||
playerSearch.dispatchEvent(new Event('input'));
|
||||
playerSearch.closest('form')!.dispatchEvent(new Event('submit'));
|
||||
const newer = http.expectOne(`${adminApi}/players?search=New&assignment=all&page=1&limit=20`);
|
||||
|
||||
const olderWasCancelled = older.cancelled;
|
||||
newer.flush(playersPage({ data: [{ ...playersPage().data[0], firstName: 'New' }] }));
|
||||
if (!olderWasCancelled) older.flush(playersPage());
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(olderWasCancelled).toBe(true);
|
||||
expect(panel.textContent).toContain('New Player');
|
||||
expect(panel.textContent).not.toContain('Linus Player');
|
||||
});
|
||||
|
||||
it('clears stale player rows on load errors and retries with load-specific copy', () => {
|
||||
isAdmin.set(true);
|
||||
create();
|
||||
flushDirectory();
|
||||
@@ -253,15 +362,23 @@ describe('Users directory', () => {
|
||||
fixture.detectChanges();
|
||||
http.expectOne(`${adminApi}/players?assignment=all&page=1&limit=20`).flush(playersPage());
|
||||
fixture.detectChanges();
|
||||
expect(text()).toContain('Linus Player');
|
||||
expect(text()).toContain('Second Team');
|
||||
const panel = (fixture.nativeElement as HTMLElement).querySelector<HTMLElement>('app-player-assignments')!;
|
||||
expect(panel.textContent).toContain('Linus Player');
|
||||
const playerSearch = panel.querySelector<HTMLInputElement>('input[type="search"]')!;
|
||||
playerSearch.value = 'broken';
|
||||
playerSearch.dispatchEvent(new Event('input'));
|
||||
playerSearch.closest('form')!.dispatchEvent(new Event('submit'));
|
||||
http.expectOne(`${adminApi}/players?search=broken&assignment=all&page=1&limit=20`).flush(
|
||||
{ message: 'Search unavailable' },
|
||||
{ status: 500, statusText: 'Server Error' },
|
||||
);
|
||||
fixture.detectChanges();
|
||||
|
||||
button('Zuordnen').click();
|
||||
const assign = http.expectOne(`${adminApi}/7/players/202`);
|
||||
expect(assign.request.method).toBe('PUT');
|
||||
assign.flush(ada);
|
||||
http.expectOne(`${api}?page=1&limit=20`).flush(directoryPage());
|
||||
http.expectOne(`${adminApi}/players?assignment=all&page=1&limit=20`).flush(playersPage());
|
||||
expect(panel.textContent).not.toContain('Linus Player');
|
||||
expect(panel.textContent).toContain('Spieler konnten nicht geladen werden');
|
||||
expect(panel.textContent).toContain('Search unavailable');
|
||||
childButton(panel, 'Erneut versuchen').click();
|
||||
http.expectOne(`${adminApi}/players?search=broken&assignment=all&page=1&limit=20`).flush(playersPage());
|
||||
});
|
||||
|
||||
it('confirms unlinking and reassignment with the affected user names', () => {
|
||||
@@ -341,11 +458,37 @@ describe('Users directory', () => {
|
||||
const unlink = http.expectOne(`${adminApi}/7/players/101`);
|
||||
expect(unlink.request.method).toBe('DELETE');
|
||||
expect(text()).toContain('Wird gelöst');
|
||||
const panel = (fixture.nativeElement as HTMLElement).querySelector<HTMLElement>('app-player-assignments')!;
|
||||
expect(panel.querySelector('section')?.getAttribute('aria-busy')).toBe('true');
|
||||
expect(childButton(panel, 'Schließen').disabled).toBe(true);
|
||||
expect(childButton(panel, 'Suchen').disabled).toBe(true);
|
||||
const mainToggle = [...(fixture.nativeElement as HTMLElement).querySelectorAll<HTMLButtonElement>('[data-user-id="7"] > .user-row__actions button')].find(
|
||||
(item) => item.textContent?.includes('Zuordnungen verwalten'),
|
||||
)!;
|
||||
expect(mainToggle.disabled).toBe(true);
|
||||
childButton(panel, 'Schließen').click();
|
||||
fixture.detectChanges();
|
||||
expect((fixture.nativeElement as HTMLElement).querySelector('app-player-assignments')).not.toBeNull();
|
||||
unlink.flush(ada);
|
||||
http.expectOne(`${api}?page=1&limit=20`).flush(directoryPage());
|
||||
http.expectOne(`${adminApi}/players?assignment=all&page=1&limit=20`).flush(playersPage());
|
||||
});
|
||||
|
||||
it('shows a persistent explanation for disabled self-deactivation', () => {
|
||||
isAdmin.set(true);
|
||||
currentUser.set({ id: 1, firstName: 'Grace', lastName: 'Admin', role: { id: 1 } });
|
||||
create();
|
||||
http.expectOne(`${api}?page=1&limit=20`).flush(directoryPage([admin]));
|
||||
fixture.detectChanges();
|
||||
const row = (fixture.nativeElement as HTMLElement).querySelector<HTMLElement>('[data-user-id="1"]')!;
|
||||
const statusButton = [...row.querySelectorAll('button')].find((item) => item.textContent?.includes('Deaktivieren'))!;
|
||||
const descriptionId = statusButton.getAttribute('aria-describedby');
|
||||
expect(descriptionId).toBeTruthy();
|
||||
expect((fixture.nativeElement as HTMLElement).querySelector(`#${descriptionId}`)?.textContent).toContain(
|
||||
'eigene Konto kann nicht deaktiviert werden',
|
||||
);
|
||||
});
|
||||
|
||||
it('renders loading, empty, general error, and retry states', () => {
|
||||
create();
|
||||
expect((fixture.nativeElement as HTMLElement).querySelector('[role="progressbar"]')).not.toBeNull();
|
||||
|
||||
Reference in New Issue
Block a user