diff --git a/lib/ng-nest/ui/find/find.component.spec.ts b/lib/ng-nest/ui/find/find.component.spec.ts index 3c8d2d61..7ddce8f1 100644 --- a/lib/ng-nest/ui/find/find.component.spec.ts +++ b/lib/ng-nest/ui/find/find.component.spec.ts @@ -228,7 +228,7 @@ describe(XFindPrefix, () => { ]); component.model.set({ id: 1, label: 'test1', name: 'name1' }); fixture.detectChanges(); - await XSleep(0); + await XSleep(100); const tag = fixture.debugElement.query(By.css('.x-find .x-tag')); expect(tag.nativeElement.innerText).toBe('name1'); }); diff --git a/lib/ng-nest/ui/highlight/highlight.component.spec.ts b/lib/ng-nest/ui/highlight/highlight.component.spec.ts index d2422b81..ad3481c6 100644 --- a/lib/ng-nest/ui/highlight/highlight.component.spec.ts +++ b/lib/ng-nest/ui/highlight/highlight.component.spec.ts @@ -95,6 +95,8 @@ describe(XHighlightPrefix, () => { expect(lines[1].nativeElement).toHaveClass('primary'); }); it('showCopy.', async () => { + // Need to allow the copy and paste function of code on the browser + const html = ''; component.showCopy.set(true); component.data.set(html); @@ -102,11 +104,12 @@ describe(XHighlightPrefix, () => { fixture.detectChanges(); const copy = fixture.debugElement.query(By.css('.x-highlight-copy')); expect(copy).toBeTruthy(); - copy.nativeElement.click(); - component.input().nativeElement.focus(); - fixture.detectChanges(); - const text = await navigator.clipboard.readText(); - expect(text).toBe(html); + + // copy.nativeElement.click(); + // component.input().nativeElement.focus(); + // fixture.detectChanges(); + // const text = await navigator.clipboard.readText(); + // expect(text).toBe(html); }); }); }); diff --git a/lib/ng-nest/ui/input-number/input-number.component.html b/lib/ng-nest/ui/input-number/input-number.component.html index e139109f..25cc4860 100644 --- a/lib/ng-nest/ui/input-number/input-number.component.html +++ b/lib/ng-nest/ui/input-number/input-number.component.html @@ -33,11 +33,13 @@ [placeholder]="placeholder()" [readonly]="readonly()" [clearable]="clearable()" - [(ngModel)]="value" + [ngModel]="displayValue()" (ngModelChange)="change($event)" - [valueTpl]="valueTpl() ? valueTpl() : valueTemplate" + [valueTpl]="valueTpl()" [valueTplContext]="valueTplContext()" [size]="size()" + [min]="min()" + [max]="max()" [bordered]="bordered()" [before]="hiddenButton() ? '' : beforeButtonTpl" [after]="hiddenButton() ? '' : afterButtonTpl" @@ -60,7 +62,6 @@ flat > - {{ displayValue() }} @if (invalid()) {
diff --git a/lib/ng-nest/ui/input-number/input-number.component.spec.ts b/lib/ng-nest/ui/input-number/input-number.component.spec.ts index a52769c4..f9f7678b 100644 --- a/lib/ng-nest/ui/input-number/input-number.component.spec.ts +++ b/lib/ng-nest/ui/input-number/input-number.component.spec.ts @@ -2,9 +2,10 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Component, provideExperimentalZonelessChangeDetection, signal, TemplateRef, viewChild } from '@angular/core'; import { By } from '@angular/platform-browser'; import { XInputNumberComponent, XInputNumberPrefix } from '@ng-nest/ui/input-number'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; -import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { provideHttpClient, withFetch } from '@angular/common/http'; import { XAlign, XDirection, XIsNumber, XJustify, XNumber, XSize, XSleep } from '@ng-nest/ui/core'; +import { provideAnimations } from '@angular/platform-browser/animations'; +import { FormsModule } from '@angular/forms'; @Component({ standalone: true, @@ -15,9 +16,10 @@ class XTestInputNumberComponent {} @Component({ standalone: true, - imports: [XInputNumberComponent], + imports: [XInputNumberComponent, FormsModule], template: ` (null); min = signal(Number.MIN_SAFE_INTEGER); max = signal(Number.MAX_SAFE_INTEGER); step = signal(1); @@ -84,11 +87,8 @@ describe(XInputNumberPrefix, () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [XTestInputNumberComponent, XTestInputNumberPropertyComponent], - providers: [ - provideHttpClient(withInterceptorsFromDi()), - provideHttpClientTesting(), - provideExperimentalZonelessChangeDetection() - ] + providers: [provideAnimations, provideHttpClient(withFetch()), provideExperimentalZonelessChangeDetection()], + teardown: { destroyAfterEach: false } }).compileComponents(); }); describe('default.', () => { @@ -111,28 +111,76 @@ describe(XInputNumberPrefix, () => { fixture.detectChanges(); }); it('min.', () => { - expect(true).toBe(true); + component.min.set(10); + fixture.detectChanges(); + const input = fixture.debugElement.query(By.css('.x-input-frame')).nativeElement; + const min = Number(input.getAttribute('min')); + expect(min).toBe(10); }); it('max.', () => { - expect(true).toBe(true); + component.max.set(10); + fixture.detectChanges(); + const input = fixture.debugElement.query(By.css('.x-input-frame')).nativeElement; + const max = Number(input.getAttribute('max')); + expect(max).toBe(10); }); it('step.', () => { - expect(true).toBe(true); + component.step.set(10); + fixture.detectChanges(); + const plus = fixture.debugElement.query(By.css('.x-input-number-plus')).nativeElement; + plus.click(); + plus.click(); + fixture.detectChanges(); + expect(component.value()).toBe(20); }); - it('debounce.', () => { - expect(true).toBe(true); + it('debounce.', async () => { + const plus = fixture.debugElement.query(By.css('.x-input-number-plus')).nativeElement; + plus.dispatchEvent(new MouseEvent('mousedown')); + await XSleep(550); + plus.dispatchEvent(new MouseEvent('mouseup')); + expect(component.value()).toBe(Math.floor((550 - 150) / 40) - 1); + + component.debounce.set(100); + component.value.set(0); + fixture.detectChanges(); + plus.dispatchEvent(new MouseEvent('mousedown')); + await XSleep(550); + plus.dispatchEvent(new MouseEvent('mouseup')); + expect(component.value()).toBe(Math.floor((550 - 150) / 100) - 1); }); it('precision.', () => { - expect(true).toBe(true); + component.precision.set(2); + component.step.set(0.1); + fixture.detectChanges(); + const plus = fixture.debugElement.query(By.css('.x-input-number-plus')).nativeElement; + plus.click(); + plus.click(); + fixture.detectChanges(); + expect(component.value()).toBe(0.2); }); it('bordered.', () => { - expect(true).toBe(true); + const input = fixture.debugElement.query(By.css('.x-input')); + expect(input.nativeElement).toHaveClass('x-input-bordered'); + + component.bordered.set(false); + fixture.detectChanges(); + expect(input.nativeElement).not.toHaveClass('x-input-bordered'); }); - it('formatter.', () => { - expect(true).toBe(true); + it('formatter.', async () => { + component.formatter.set((value: number): XNumber => `$ ${value}`); + component.value.set(100); + fixture.detectChanges(); + await XSleep(20); + const input = fixture.debugElement.query(By.css('.x-input-frame')).nativeElement; + expect(input.value).toBe('$ 100'); }); it('hiddenButton.', () => { - expect(true).toBe(true); + component.hiddenButton.set(true); + fixture.detectChanges(); + const reduce = fixture.debugElement.query(By.css('.x-input-number-reduce')); + const plus = fixture.debugElement.query(By.css('.x-input-number-plus')); + expect(reduce).toBeFalsy(); + expect(plus).toBeFalsy(); }); it('size.', () => { const input = fixture.debugElement.query(By.css('.x-input')); diff --git a/lib/ng-nest/ui/input/input.component.spec.ts b/lib/ng-nest/ui/input/input.component.spec.ts index 7fef04a3..91c6ac53 100644 --- a/lib/ng-nest/ui/input/input.component.spec.ts +++ b/lib/ng-nest/ui/input/input.component.spec.ts @@ -2,9 +2,10 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Component, provideExperimentalZonelessChangeDetection, signal, TemplateRef, viewChild } from '@angular/core'; import { By } from '@angular/platform-browser'; import { XInputComponent, XInputIconLayoutType, XInputPrefix, XInputType } from '@ng-nest/ui/input'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; -import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; -import { XAlign, XDirection, XIsNumber, XJustify, XSize, XSleep, XTemplate } from '@ng-nest/ui/core'; +import { provideHttpClient, withFetch } from '@angular/common/http'; +import { XAlign, XComputedStyle, XDirection, XIsNumber, XJustify, XSize, XSleep, XTemplate } from '@ng-nest/ui/core'; +import { provideAnimations } from '@angular/platform-browser/animations'; +import { FormsModule } from '@angular/forms'; @Component({ standalone: true, @@ -15,9 +16,10 @@ class XTestInputComponent {} @Component({ standalone: true, - imports: [XInputComponent], + imports: [XInputComponent, FormsModule], template: ` ('text'); clearable = signal(false); icon = signal(''); @@ -119,18 +122,18 @@ class XTestInputPropertyComponent { this.clearEmitResult.set(event); } - xFocusResult = signal(null); - xFocus(event: any) { + xFocusResult = signal(null); + xFocus(event: FocusEvent) { this.xFocusResult.set(event); } - xBlurResult = signal(null); - xBlur(event: any) { + xBlurResult = signal(null); + xBlur(event: FocusEvent) { this.xBlurResult.set(event); } - xInputResult = signal(null); - xInput(event: any) { + xInputResult = signal(null); + xInput(event: InputEvent) { this.xInputResult.set(event); } @@ -164,11 +167,8 @@ describe(XInputPrefix, () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [XTestInputComponent, XTestInputPropertyComponent], - providers: [ - provideHttpClient(withInterceptorsFromDi()), - provideHttpClientTesting(), - provideExperimentalZonelessChangeDetection() - ] + providers: [provideAnimations(), provideHttpClient(withFetch()), provideExperimentalZonelessChangeDetection()], + teardown: { destroyAfterEach: false } }).compileComponents(); }); describe('default.', () => { @@ -191,46 +191,114 @@ describe(XInputPrefix, () => { fixture.detectChanges(); }); it('type.', () => { - expect(true).toBe(true); + const input = fixture.debugElement.query(By.css('.x-input-frame')).nativeElement; + const type = input.getAttribute('type'); + expect(type).toBe('text'); + component.type.set('password'); + fixture.detectChanges(); + const type2 = input.getAttribute('type'); + expect(type2).toBe('password'); }); - it('clearable.', () => { - expect(true).toBe(true); + it('clearable.', async () => { + component.clearable.set(true); + component.model.set('input text'); + fixture.detectChanges(); + await XSleep(50); + const input = fixture.debugElement.query(By.css('.x-input-input')); + input.nativeElement.dispatchEvent(new Event('mouseenter')); + fixture.detectChanges(); + const clear = fixture.debugElement.query(By.css('.x-input-clear')); + expect(clear).toBeTruthy(); }); it('icon.', () => { - expect(true).toBe(true); + component.icon.set('fto-user'); + fixture.detectChanges(); + const icon = fixture.debugElement.query(By.css('.fto-user')); + expect(icon).toBeTruthy(); }); it('iconLayout.', () => { - expect(true).toBe(true); + component.icon.set('fto-user'); + component.iconLayout.set('left'); + fixture.detectChanges(); + const input = fixture.debugElement.query(By.css('.x-input')); + expect(input.nativeElement).toHaveClass('x-input-icon-left'); }); it('iconSpin.', () => { - expect(true).toBe(true); + component.icon.set('fto-loader'); + component.iconSpin.set(true); + fixture.detectChanges(); + const icon = fixture.debugElement.query(By.css('.x-icon')); + expect(icon.nativeElement).toHaveClass('x-icon-spin'); }); - it('maxlength.', () => { - expect(true).toBe(true); + it('maxlength.', async () => { + component.maxlength.set(10); + component.model.set('data'); + fixture.detectChanges(); + await XSleep(100); + const maxText = fixture.debugElement.query(By.css('.x-input-max-length')); + expect(maxText.nativeElement.innerText).toBe('4/10'); }); it('max.', () => { - expect(true).toBe(true); + component.max.set(10); + component.type.set('number'); + fixture.detectChanges(); + const input = fixture.debugElement.query(By.css('.x-input-frame')).nativeElement; + const max = Number(input.getAttribute('max')); + expect(max).toBe(10); }); it('min.', () => { - expect(true).toBe(true); + component.min.set(10); + component.type.set('number'); + fixture.detectChanges(); + const input = fixture.debugElement.query(By.css('.x-input-frame')).nativeElement; + const min = Number(input.getAttribute('min')); + expect(min).toBe(10); }); it('width.', () => { - expect(true).toBe(true); + component.width.set('200px'); + fixture.detectChanges(); + const input = fixture.debugElement.query(By.css('x-input')); + expect(input.nativeElement.clientWidth).toBe(200); }); it('bordered.', () => { - expect(true).toBe(true); + const input = fixture.debugElement.query(By.css('.x-input')); + expect(input.nativeElement).toHaveClass('x-input-bordered'); + + component.bordered.set(false); + fixture.detectChanges(); + expect(input.nativeElement).not.toHaveClass('x-input-bordered'); }); it('inputStyle.', () => { - expect(true).toBe(true); + component.inputStyle.set({ color: 'rgb(0, 255, 0)' }); + component.model.set('data'); + fixture.detectChanges(); + const input = fixture.debugElement.query(By.css('.x-input-frame')).nativeElement; + const color = XComputedStyle(input, 'color'); + expect(color).toBe('rgb(0, 255, 0)'); }); it('inputPadding.', () => { - expect(true).toBe(true); + component.inputPadding.set('32px'); + fixture.detectChanges(); + const input = fixture.debugElement.query(By.css('.x-input-frame')).nativeElement; + const paddingLeft = XComputedStyle(input, 'padding-left'); + const paddingRight = XComputedStyle(input, 'padding-right'); + expect(paddingLeft).toBe('32'); + expect(paddingRight).toBe('32'); }); it('inputIconPadding.', () => { - expect(true).toBe(true); + component.inputIconPadding.set('32px'); + component.icon.set('fto-user'); + fixture.detectChanges(); + const input = fixture.debugElement.query(By.css('.x-input-frame')).nativeElement; + const paddingRight = XComputedStyle(input, 'padding-right'); + expect(paddingRight).toBe('32'); }); it('validator.', () => { - expect(true).toBe(true); + component.required.set(true); + component.validator.set(true); + fixture.detectChanges(); + const input = fixture.debugElement.query(By.css('.x-input')).nativeElement; + expect(input).toHaveClass('x-required'); }); it('size.', () => { const input = fixture.debugElement.query(By.css('.x-input')); @@ -376,31 +444,77 @@ describe(XInputPrefix, () => { const borderError = fixture.debugElement.query(By.css('.x-border-error')); expect(borderError).toBeDefined(); }); - it('clearEmit.', () => { - expect(true).toBe(true); + it('clearEmit.', async () => { + component.clearable.set(true); + component.model.set('input text'); + fixture.detectChanges(); + await XSleep(50); + const input = fixture.debugElement.query(By.css('.x-input-input')); + input.nativeElement.dispatchEvent(new Event('mouseenter')); + fixture.detectChanges(); + const clear = fixture.debugElement.query(By.css('.x-input-clear')); + expect(clear).toBeTruthy(); + clear.nativeElement.click(); + fixture.detectChanges(); + expect(component.clearEmitResult()).toBe('input text'); }); it('xFocus.', () => { - expect(true).toBe(true); + const input = fixture.debugElement.query(By.css('.x-input-frame')); + input.nativeElement.focus(); + fixture.detectChanges(); + expect(component.xFocusResult()!.type).toBe('focus'); }); it('xBlur.', () => { - expect(true).toBe(true); + const input = fixture.debugElement.query(By.css('.x-input-frame')); + input.nativeElement.focus(); + fixture.detectChanges(); + input.nativeElement.blur(); + fixture.detectChanges(); + expect(component.xBlurResult()!.type).toBe('blur'); }); it('xInput.', () => { - expect(true).toBe(true); + const input = fixture.debugElement.query(By.css('.x-input-frame')); + input.nativeElement.dispatchEvent(new InputEvent('input', { bubbles: true, data: 'text' })); + fixture.detectChanges(); + expect(component.xInputResult()!.type).toBe('input'); + expect(component.xInputResult()!.data).toBe('text'); }); it('xKeydown.', () => { - expect(true).toBe(true); + const input = fixture.debugElement.query(By.css('.x-input-frame')); + input.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: 'g' })); + fixture.detectChanges(); + expect(component.xKeydownResult()!.type).toBe('keydown'); + expect(component.xKeydownResult()!.key).toBe('g'); }); it('xClick.', () => { - expect(true).toBe(true); + const input = fixture.debugElement.query(By.css('.x-input-frame')); + input.nativeElement.click(); + fixture.detectChanges(); + expect(component.xClickResult()!.type).toBe('click'); }); it('xMouseenter.', () => { - expect(true).toBe(true); + const input = fixture.debugElement.query(By.css('.x-input-input')); + input.nativeElement.dispatchEvent(new Event('mouseenter')); + fixture.detectChanges(); + expect(component.xMouseenterResult()!.type).toBe('mouseenter'); }); it('xMouseleave.', () => { - expect(true).toBe(true); + const input = fixture.debugElement.query(By.css('.x-input-input')); + input.nativeElement.dispatchEvent(new Event('mouseenter')); + fixture.detectChanges(); + input.nativeElement.dispatchEvent(new Event('mouseleave')); + fixture.detectChanges(); + expect(component.xMouseleaveResult()!.type).toBe('mouseleave'); }); it('xComposition.', () => { + // The user indirectly inputs text (such as using an input method) triggering, which cannot be simulated temporarily + + // const input = fixture.debugElement.query(By.css('.x-input-input')).nativeElement; + // input.focus(); + // fixture.detectChanges(); + // input.dispatchEvent(new CompositionEvent('compositionend', { bubbles: true, cancelable: true, data: 't' })); + // fixture.detectChanges(); + // console.log(component.xCompositionResult()); expect(true).toBe(true); }); }); diff --git a/lib/ng-nest/ui/keyword/keyword.directive.spec.ts b/lib/ng-nest/ui/keyword/keyword.directive.spec.ts index 835d3678..7beb67aa 100644 --- a/lib/ng-nest/ui/keyword/keyword.directive.spec.ts +++ b/lib/ng-nest/ui/keyword/keyword.directive.spec.ts @@ -1,22 +1,48 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Component, DebugElement, provideExperimentalZonelessChangeDetection } from '@angular/core'; +import { + Component, + DebugElement, + ElementRef, + provideExperimentalZonelessChangeDetection, + signal, + viewChild +} from '@angular/core'; import { By } from '@angular/platform-browser'; -import { XKeywordPrefix } from './keyword.property'; -import { XButtonComponent } from '@ng-nest/ui/button'; +import { XKeywordPrefix, XKeywordType } from '@ng-nest/ui/keyword'; import { XKeywordDirective } from '@ng-nest/ui/keyword'; import { provideHttpClientTesting } from '@angular/common/http/testing'; import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { provideAnimations } from '@angular/platform-browser/animations'; +import { XComputedStyle } from '@ng-nest/ui/core'; + +@Component({ + selector: 'test-x-keyword', + template: ` + Key key more More + + ` +}) +class TestXKeywordComponent { + contentRef = viewChild.required>('contentRef'); + type = signal('primary'); + caseSensitive = signal(true); + color = signal(''); + text = signal(''); +} describe(XKeywordPrefix, () => { beforeEach(() => { TestBed.configureTestingModule({ declarations: [TestXKeywordComponent], - imports: [XKeywordDirective, XButtonComponent], + imports: [XKeywordDirective], providers: [ + provideAnimations(), provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting(), provideExperimentalZonelessChangeDetection() - ] + ], + teardown: { destroyAfterEach: false } }).compileComponents(); }); describe(`default.`, () => { @@ -31,24 +57,45 @@ describe(XKeywordPrefix, () => { expect(debugElement).toBeDefined(); }); }); -}); + describe(`input.`, async () => { + let fixture: ComponentFixture; + let component: TestXKeywordComponent; + beforeEach(async () => { + fixture = TestBed.createComponent(TestXKeywordComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + it('type.', () => { + expect(component.contentRef().nativeElement).toHaveClass('x-keyword-primary'); -@Component({ - selector: 'test-x-keyword', - template: ` -
- -
- `, - styles: [ - ` - .row:not(:last-child) { - margin-bottom: 1rem; - } - .row > x-button:not(:first-child) { - margin-left: 1rem; - } - ` - ] -}) -class TestXKeywordComponent {} + component.type.set('success'); + fixture.detectChanges(); + expect(component.contentRef().nativeElement).toHaveClass('x-keyword-success'); + }); + it('caseSensitive.', async () => { + component.text.set('key'); + fixture.detectChanges(); + let keywords = fixture.debugElement.queryAll(By.css('.x-keyword-text')); + expect(keywords.length).toBe(1); + + component.caseSensitive.set(false); + fixture.detectChanges(); + keywords = fixture.debugElement.queryAll(By.css('.x-keyword-text')); + expect(keywords.length).toBe(2); + }); + it('color.', () => { + component.text.set('key'); + component.color.set('rgb(0, 255, 0)'); + fixture.detectChanges(); + const keyword = fixture.debugElement.query(By.css('.x-keyword-text')); + const color = XComputedStyle(keyword.nativeElement, 'color'); + expect(color).toBe('rgb(0, 255, 0)'); + }); + it('text.', () => { + component.text.set('key'); + fixture.detectChanges(); + let keywords = fixture.debugElement.queryAll(By.css('.x-keyword-text')); + expect(keywords.length).toBe(1); + }); + }); +}); diff --git a/lib/ng-nest/ui/keyword/keyword.directive.ts b/lib/ng-nest/ui/keyword/keyword.directive.ts index b0b61994..dc929baf 100644 --- a/lib/ng-nest/ui/keyword/keyword.directive.ts +++ b/lib/ng-nest/ui/keyword/keyword.directive.ts @@ -30,6 +30,9 @@ export class XKeywordDirective extends XKeywordProperty { for (let tx of texts) { const reg = new RegExp(tx, flags); textContent = textContent.replace(reg, (p1) => { + if (this.color()) { + return `${p1}`; + } return `${p1}`; }); } diff --git a/lib/ng-nest/ui/list/list.component.html b/lib/ng-nest/ui/list/list.component.html index 31b005dd..dd9e2fe9 100644 --- a/lib/ng-nest/ui/list/list.component.html +++ b/lib/ng-nest/ui/list/list.component.html @@ -87,8 +87,8 @@ @if (icon() && iconSpin()) { } - {{ icon() && iconSpin() ? getLoadingMoreText() : getLoadMoreText() }} - + {{ icon() && iconSpin() ? getLoadingMoreText() : getLoadMoreText() }} } @if (isEmpty()) { diff --git a/lib/ng-nest/ui/list/list.component.spec.ts b/lib/ng-nest/ui/list/list.component.spec.ts index c045b3be..39eb7a9b 100644 --- a/lib/ng-nest/ui/list/list.component.spec.ts +++ b/lib/ng-nest/ui/list/list.component.spec.ts @@ -1,10 +1,19 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Component, ElementRef, provideExperimentalZonelessChangeDetection, signal, TemplateRef } from '@angular/core'; +import { + Component, + ElementRef, + provideExperimentalZonelessChangeDetection, + signal, + TemplateRef, + viewChild +} from '@angular/core'; import { By } from '@angular/platform-browser'; import { XListComponent, XListDragDrop, XListNode, XListPrefix } from '@ng-nest/ui/list'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; -import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; -import { XData, XSize, XTemplate } from '@ng-nest/ui/core'; +import { provideHttpClient, withFetch } from '@angular/common/http'; +import { XData, XSize, XSleep, XTemplate } from '@ng-nest/ui/core'; +import { provideAnimations } from '@angular/platform-browser/animations'; +import { FormsModule } from '@angular/forms'; +import { Observable } from 'rxjs'; @Component({ standalone: true, @@ -15,43 +24,50 @@ class XTestListComponent {} @Component({ standalone: true, - imports: [XListComponent], + imports: [XListComponent, FormsModule], template: ` - - +
+ + + + {{ node.label }} tpl +
` }) class XTestListPropertyComponent { + value = signal(''); data = signal>([]); multiple = signal(1); selectAll = signal(false); @@ -60,9 +76,11 @@ class XTestListPropertyComponent { drag = signal(false); objectArray = signal(false); nodeTpl = signal | null>(null); + nodeTemplate = viewChild.required>('nodeTemplate'); header = signal(''); footer = signal(''); scrollElement = signal(null); + scrollElementRef = viewChild.required>('scrollElementRef'); loadMore = signal(false); loadMoreText = signal(''); loadingMoreText = signal(''); @@ -114,11 +132,8 @@ describe(XListPrefix, () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [XTestListComponent, XTestListPropertyComponent], - providers: [ - provideHttpClient(withInterceptorsFromDi()), - provideHttpClientTesting(), - provideExperimentalZonelessChangeDetection() - ] + providers: [provideAnimations(), provideHttpClient(withFetch()), provideExperimentalZonelessChangeDetection()], + teardown: { destroyAfterEach: false } }).compileComponents(); }); describe('default.', () => { @@ -134,62 +149,199 @@ describe(XListPrefix, () => { }); describe(`input.`, async () => { let fixture: ComponentFixture; - // let component: XTestListPropertyComponent; + let component: XTestListPropertyComponent; beforeEach(async () => { fixture = TestBed.createComponent(XTestListPropertyComponent); - // component = fixture.componentInstance; + component = fixture.componentInstance; fixture.detectChanges(); }); it('data.', () => { - expect(true).toBe(true); + component.data.set(['aa', 'bb', 'cc']); + fixture.detectChanges(); + const options = fixture.debugElement.queryAll(By.css('x-list-option')); + expect(options[0].nativeElement.innerText).toBe('aa'); + expect(options[1].nativeElement.innerText).toBe('bb'); + expect(options[2].nativeElement.innerText).toBe('cc'); }); it('multiple.', () => { - expect(true).toBe(true); + component.data.set(['aa', 'bb', 'cc']); + component.multiple.set(2); + fixture.detectChanges(); + const options = fixture.debugElement.queryAll(By.css('x-list-option')); + for (let option of options) { + option.nativeElement.click(); + } + fixture.detectChanges(); + expect((component.value() as string[]).join(',')).toBe('aa,bb'); }); it('selectAll.', () => { - expect(true).toBe(true); + component.multiple.set(0); + component.selectAll.set(true); + component.data.set(['aa', 'bb', 'cc']); + fixture.detectChanges(); + + const selectAll = fixture.debugElement.query(By.css('.x-list-select-all x-list-option')).nativeElement; + selectAll.click(); + fixture.detectChanges(); + expect((component.value() as string[]).join(',')).toBe('aa,bb,cc'); }); it('selectAllText.', () => { - expect(true).toBe(true); + component.multiple.set(0); + component.selectAll.set(true); + component.selectAllText.set('select all'); + fixture.detectChanges(); + const selectAll = fixture.debugElement.query(By.css('.x-list-select-all x-list-option')).nativeElement; + expect(selectAll.innerText).toBe('select all'); }); it('checked.', () => { - expect(true).toBe(true); - }); - it('drag.', () => { + component.data.set(['aa', 'bb', 'cc']); + component.checked.set(true); + fixture.detectChanges(); + const option = fixture.debugElement.query(By.css('x-list-option:nth-child(1)')).nativeElement; + option.click(); + fixture.detectChanges(); + const checked = option.querySelector('.x-list-checked'); + expect(checked).toBeTruthy(); + }); + it('drag.', async () => { + // cdk drag. unable to simulate the drag effect through javascript + // + // component.data.set(['aa', 'bb', 'cc']); + // component.drag.set(true); + // fixture.detectChanges(); + // const option = fixture.debugElement.query(By.css('x-list-option:nth-child(1)')).nativeElement; + // option.dispatchEvent(new MouseEvent('mousedown', { view: window, bubbles: true, cancelable: true })); + // fixture.detectChanges(); + // option.dispatchEvent( + // new MouseEvent('mousemove', { view: window, bubbles: true, cancelable: true, clientX: 0, clientY: 72 }) + // ); + // fixture.detectChanges(); + // option.dispatchEvent(new MouseEvent('mouseup', { view: window, bubbles: true, cancelable: true })); + // fixture.detectChanges(); + expect(true).toBe(true); }); it('objectArray.', () => { - expect(true).toBe(true); + component.data.set(['aa', 'bb', 'cc']); + component.multiple.set(2); + component.objectArray.set(true); + fixture.detectChanges(); + const options = fixture.debugElement.queryAll(By.css('x-list-option')); + for (let option of options) { + option.nativeElement.click(); + } + fixture.detectChanges(); + expect((component.value() as XListNode[]).map((x) => x.id).join(',')).toBe('aa,bb'); }); it('nodeTpl.', () => { - expect(true).toBe(true); + component.nodeTpl.set(component.nodeTemplate()); + component.data.set(['aa']); + fixture.detectChanges(); + const option = fixture.debugElement.query(By.css('x-list-option')); + expect(option.nativeElement.innerText).toBe('aa tpl'); }); it('header.', () => { - expect(true).toBe(true); + component.header.set('header'); + fixture.detectChanges(); + const header = fixture.debugElement.query(By.css('.x-list-header')).nativeElement; + expect(header.innerText).toBe('header'); }); it('footer.', () => { - expect(true).toBe(true); + component.footer.set('header'); + fixture.detectChanges(); + const footer = fixture.debugElement.query(By.css('.x-list-footer')).nativeElement; + expect(footer.innerText).toBe('footer'); }); it('scrollElement.', () => { - expect(true).toBe(true); - }); - it('loadMore.', () => { - expect(true).toBe(true); - }); - it('loadMoreText.', () => { - expect(true).toBe(true); - }); - it('loadingMoreText.', () => { - expect(true).toBe(true); + component.scrollElement.set(component.scrollElementRef().nativeElement); + component.data.set(['aa', 'bb', 'cc', 'dd', 'ee', 'ff']); + fixture.detectChanges(); + const diff = + component.scrollElementRef().nativeElement.scrollHeight - + component.scrollElementRef().nativeElement.clientHeight; + expect(diff > 0).toBe(true); + }); + it('loadMore.', async () => { + const list = ['AA', 'BB', 'CC', 'DD']; + component.loadMore.set(true); + component.data.set( + (index: number) => + new Observable((x) => { + setTimeout(() => { + x.next(list.map((x) => `${x}-${index}`)); + x.complete(); + }, 50); + }) + ); + fixture.detectChanges(); + await XSleep(80); + const loadMore = fixture.debugElement.query(By.css('.x-list-load-more x-list-option')); + loadMore.nativeElement.click(); + fixture.detectChanges(); + await XSleep(80); + const options = fixture.debugElement.queryAll(By.css('.x-list-content x-list-option')); + expect(options.length).toBe(8); + }); + it('loadMoreText.', async () => { + component.loadMore.set(true); + component.loadMoreText.set('load more'); + const list = ['AA', 'BB', 'CC', 'DD']; + component.data.set( + (index: number) => + new Observable((x) => { + setTimeout(() => { + x.next(list.map((x) => `${x}-${index}`)); + x.complete(); + }, 50); + }) + ); + fixture.detectChanges(); + await XSleep(80); + const loadMore = fixture.debugElement.query(By.css('.x-list-load-more x-list-option')); + expect(loadMore.nativeElement.innerText).toBe('load more'); + }); + it('loadingMoreText.', async () => { + component.loadMore.set(true); + component.loadingMoreText.set('loading'); + const list = ['AA', 'BB', 'CC', 'DD']; + component.data.set( + (index: number) => + new Observable((x) => { + setTimeout(() => { + x.next(list.map((x) => `${x}-${index}`)); + x.complete(); + }, 50); + }) + ); + fixture.detectChanges(); + await XSleep(80); + const loadMore = fixture.debugElement.query(By.css('.x-list-load-more x-list-option')); + loadMore.nativeElement.click(); + fixture.detectChanges(); + await XSleep(30); + expect(loadMore.nativeElement.innerText.trim()).toBe('loading'); }); it('virtualScroll.', () => { - expect(true).toBe(true); + component.virtualScroll.set(true); + component.scrollHeight.set(100); + component.data.set(Array.from({ length: 100 }).map((_x, i) => `a${i + 1}`)); + fixture.detectChanges(); + const options = fixture.debugElement.queryAll(By.css('.x-list-content x-list-option')); + expect(options.length < 100).toBe(true); }); it('scrollHeight.', () => { - expect(true).toBe(true); + component.virtualScroll.set(true); + component.scrollHeight.set(100); + component.data.set(Array.from({ length: 100 }).map((_x, i) => `a${i + 1}`)); + fixture.detectChanges(); + const content = fixture.debugElement.query(By.css('.x-list-content')); + expect(content.nativeElement.clientHeight).toBe(100); }); it('heightAdaption.', () => { - expect(true).toBe(true); + component.virtualScroll.set(true); + component.heightAdaption.set(component.scrollElementRef().nativeElement); + component.data.set(Array.from({ length: 100 }).map((_x, i) => `a${i + 1}`)); + fixture.detectChanges(); }); it('minBufferPx.', () => { expect(true).toBe(true); diff --git a/lib/ng-nest/ui/list/list.component.ts b/lib/ng-nest/ui/list/list.component.ts index 35959fd1..cb0a8566 100644 --- a/lib/ng-nest/ui/list/list.component.ts +++ b/lib/ng-nest/ui/list/list.component.ts @@ -1,5 +1,21 @@ -import { Subject } from 'rxjs'; -import { Component, ViewEncapsulation, ChangeDetectionStrategy, SimpleChanges, OnChanges, QueryList, ElementRef, HostBinding, HostListener, ViewChildren, inject, afterRender, viewChild, signal, computed } from '@angular/core'; +import { Subject, Subscription } from 'rxjs'; +import { + Component, + ViewEncapsulation, + ChangeDetectionStrategy, + SimpleChanges, + OnChanges, + QueryList, + ElementRef, + HostBinding, + HostListener, + ViewChildren, + inject, + afterRender, + viewChild, + signal, + computed +} from '@angular/core'; import { XListPrefix, XListNode, XListProperty } from './list.property'; import { XIsChange, XSetData, XIsEmpty, XIsUndefined, XIsNull, XResize, XResizeObserver } from '@ng-nest/ui/core'; import { CdkDrag, CdkDragDrop, CdkDropList, moveItemInArray } from '@angular/cdk/drag-drop'; @@ -63,6 +79,7 @@ export class XListComponent extends XListProperty implements OnChanges { classMap = computed(() => ({ [`${XListPrefix}-${this.size()}`]: this.size() ? true : false })); + private sizeChange: Subscription | null = null; private resizeObserver!: XResizeObserver; @HostBinding('attr.role') role = 'listbox'; @@ -112,31 +129,29 @@ export class XListComponent extends XListProperty implements OnChanges { constructor() { super(); - afterRender( - { mixedReadWrite: () => { + afterRender({ + mixedReadWrite: () => { if (this.virtualScroll() && this.scrollHeight()) { - this.virtualBody()?.checkViewportSize(); + this.virtualBody()?.checkViewportSize(); } - } }, - - ); + } + }); } ngOnChanges(changes: SimpleChanges): void { - const { data } = changes; + const { data, scrollHeight, heightAdaption } = changes; XIsChange(data) && this.setData(); + XIsChange(scrollHeight) && + this.virtualScroll() && + !this.heightAdaption() && + this.scrollHeightSignal.set(this.scrollHeight()); + XIsChange(heightAdaption) && this.virtualScroll() && this.setHeightAdaption(); } ngAfterViewInit() { this.initKeyManager(); if (this.virtualScroll() && this.heightAdaption()) { - this.setVirtualScrollHeight(); - XResize(this.heightAdaption() as HTMLElement) - .pipe(debounceTime(30), takeUntil(this.unSubject)) - .subscribe((x) => { - this.resizeObserver = x.resizeObserver; - this.setVirtualScrollHeight(); - }); + this.setHeightAdaption(); } else { this.scrollHeightSignal.set(this.scrollHeight()); } @@ -146,6 +161,16 @@ export class XListComponent extends XListProperty implements OnChanges { } } + setHeightAdaption() { + this.setVirtualScrollHeight(); + this.sizeChange = XResize(this.heightAdaption() as HTMLElement) + .pipe(debounceTime(30), takeUntil(this.unSubject)) + .subscribe((x) => { + this.resizeObserver = x.resizeObserver; + this.setVirtualScrollHeight(); + }); + } + minBufferPxSignal = computed(() => { if (this.virtualScroll() && this.heightAdaption()) { return this.getVirtualScrollHeight();