|
@@ -49,6 +49,85 @@ describe('OperationsUsersComponent', () => {
|
|
|
expect((fixture.nativeElement as HTMLElement).textContent).toContain('注册时间至');
|
|
expect((fixture.nativeElement as HTMLElement).textContent).toContain('注册时间至');
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ for (const size of [20, 30, 50, 100]) {
|
|
|
|
|
+ it(`loads the first page with ${size} members after changing the page size`, async () => {
|
|
|
|
|
+ const component = fixture.componentInstance;
|
|
|
|
|
+ component.page.set({ ...memberPage, page: 3, total: 172 });
|
|
|
|
|
+ operations.users.and.callFake(async (page, pageSize) => ({ ...memberPage, page, pageSize, total: 172 }));
|
|
|
|
|
+ fixture.detectChanges();
|
|
|
|
|
+ const select = (fixture.nativeElement as HTMLElement).querySelector<HTMLSelectElement>('.page-size select')!;
|
|
|
|
|
+ expect(select).not.toBeNull();
|
|
|
|
|
+ if (!select) return;
|
|
|
|
|
+ expect(Array.from(select.options, option => option.textContent?.trim())).toEqual(['20 条', '30 条', '50 条', '100 条', '全部']);
|
|
|
|
|
+ select.selectedIndex = [20, 30, 50, 100].indexOf(size);
|
|
|
|
|
+ select.dispatchEvent(new Event('change', { bubbles: true }));
|
|
|
|
|
+ await fixture.whenStable();
|
|
|
|
|
+ fixture.detectChanges();
|
|
|
|
|
+ expect(operations.users).toHaveBeenCalledWith(1, size, '', jasmine.any(Object), { field: 'registeredAt', direction: 'desc' }, false);
|
|
|
|
|
+ expect(component.page()?.page).toBe(1);
|
|
|
|
|
+ expect(component.pageCount()).toBe(Math.ceil(172 / size));
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ it('loads every filtered member beyond the server page limit and disables pagination', async () => {
|
|
|
|
|
+ const component = fixture.componentInstance;
|
|
|
|
|
+ component.query.set('测试');
|
|
|
|
|
+ component.parentUserId.set(7);
|
|
|
|
|
+ // Only objectId is needed by this pagination test; no real member data is used.
|
|
|
|
|
+ const rows = Array.from({ length: 237 }, (_, index) => ({ objectId: `member-${index}` })) as MemberPage['items'];
|
|
|
|
|
+ operations.users.calls.reset();
|
|
|
|
|
+ operations.users.and.callFake(async (page, pageSize) => ({
|
|
|
|
|
+ ...memberPage, page, pageSize, total: rows.length,
|
|
|
|
|
+ items: rows.slice((page - 1) * pageSize, page * pageSize),
|
|
|
|
|
+ }));
|
|
|
|
|
+ const select = (fixture.nativeElement as HTMLElement).querySelector<HTMLSelectElement>('.page-size select')!;
|
|
|
|
|
+ expect(select).not.toBeNull();
|
|
|
|
|
+ if (!select) return;
|
|
|
|
|
+ select.selectedIndex = 4;
|
|
|
|
|
+ select.dispatchEvent(new Event('change', { bubbles: true }));
|
|
|
|
|
+ await fixture.whenStable();
|
|
|
|
|
+ fixture.detectChanges();
|
|
|
|
|
+
|
|
|
|
|
+ expect(component.page()?.items).toEqual(rows);
|
|
|
|
|
+ expect(component.pageCount()).toBe(1);
|
|
|
|
|
+ expect(operations.users.calls.allArgs().map(args => args.slice(0, 3))).toEqual([[1, 100, '测试'], [2, 100, '测试'], [3, 100, '测试']]);
|
|
|
|
|
+ for (const args of operations.users.calls.allArgs()) expect(args[3]?.['parentUserId']).toBe(7);
|
|
|
|
|
+ expect((fixture.nativeElement as HTMLElement).querySelector<HTMLButtonElement>('[aria-label="上一页"]')?.disabled).toBeTrue();
|
|
|
|
|
+ expect((fixture.nativeElement as HTMLElement).querySelector<HTMLButtonElement>('[aria-label="下一页"]')?.disabled).toBeTrue();
|
|
|
|
|
+
|
|
|
|
|
+ operations.users.calls.reset();
|
|
|
|
|
+ await component.refresh();
|
|
|
|
|
+ expect(component.page()?.items.length).toBe(237);
|
|
|
|
|
+ expect(operations.users.calls.allArgs().every(args => args[5] === true)).toBeTrue();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('opens the page-size menu above the footer so all five choices fit on screen', () => {
|
|
|
|
|
+ const trigger = (fixture.nativeElement as HTMLElement).querySelector<HTMLButtonElement>('.page-size .xs-dropdown-trigger')!;
|
|
|
|
|
+ trigger.style.position = 'fixed';
|
|
|
|
|
+ trigger.style.bottom = '20px';
|
|
|
|
|
+ trigger.click();
|
|
|
|
|
+ const menu = document.querySelector<HTMLElement>('.xs-dropdown-menu')!;
|
|
|
|
|
+ expect(menu.getBoundingClientRect().bottom).toBeLessThanOrEqual(trigger.getBoundingClientRect().top - 5);
|
|
|
|
|
+ expect(menu.querySelectorAll('button').length).toBe(5);
|
|
|
|
|
+ expect(menu.scrollHeight).toBeLessThanOrEqual(menu.clientHeight + 1);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ for (const scenario of ['changed total', 'duplicate member']) {
|
|
|
|
|
+ it(`keeps the previous page and reports an error for all-mode batches with a ${scenario}`, async () => {
|
|
|
|
|
+ const component = fixture.componentInstance, previous = component.page();
|
|
|
|
|
+ const rows = Array.from({ length: 120 }, (_, index) => ({ objectId: `member-${index}` })) as MemberPage['items'];
|
|
|
|
|
+ operations.users.and.callFake(async (page, pageSize) => ({
|
|
|
|
|
+ ...memberPage, page, pageSize,
|
|
|
|
|
+ total: page === 2 && scenario === 'changed total' ? 121 : 120,
|
|
|
|
|
+ items: page === 1 ? rows.slice(0, 100) : scenario === 'duplicate member' ? [rows[0], ...rows.slice(101)] : rows.slice(100),
|
|
|
|
|
+ }));
|
|
|
|
|
+ await component.changePageSize('all');
|
|
|
|
|
+ expect(component.page()).toBe(previous);
|
|
|
|
|
+ expect(component.error()).toContain('会员数据已变化');
|
|
|
|
|
+ expect(component.loading()).toBeFalse();
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
it('shows an explicit searching state until the member query finishes', async () => {
|
|
it('shows an explicit searching state until the member query finishes', async () => {
|
|
|
const component = fixture.componentInstance;
|
|
const component = fixture.componentInstance;
|
|
|
let resolveRequest!: (value: MemberPage) => void;
|
|
let resolveRequest!: (value: MemberPage) => void;
|