Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 58 additions & 16 deletions packages/components/textarea/e2e.playwright-spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
import { expect, Locator, Page, test } from '@playwright/test';
import { e2eEnableDarkTheme } from '../../e2e/utils';

const getHeight = async (locator: Locator): Promise<number> => {
await expect(locator).toBeVisible();
const box = await locator.boundingBox();

expect(box).not.toBeNull();

return box!.height;
Comment thread
artembelik marked this conversation as resolved.
};

const pasteFromClipboard = async (page: Page, textarea: Locator, text: string) => {
await page.evaluate((t) => navigator.clipboard.writeText(t), text);
await textarea.click();
await page.keyboard.press('ControlOrMeta+a');
await page.keyboard.press('ControlOrMeta+v');
};

test.describe('KbqTextareaModule', () => {
test.describe('E2eTextareaStates', () => {
const getComponent = (page: Page): Locator => page.getByTestId('e2eTextareaStates');
Expand All @@ -16,18 +32,6 @@ test.describe('KbqTextareaModule', () => {
test.describe('E2eTextareaGrowBehavior', () => {
const getTextarea = (page: Page): Locator => page.getByTestId('grow_textarea');

// Wait for the locator to be visible, then resolve its rendered height.
// Auto-retries via `toBeVisible` and asserts the bounding box is non-null
// so a transient detach surfaces as a readable expect failure, not a TypeError.
const getHeight = async (locator: Locator): Promise<number> => {
await expect(locator).toBeVisible();
const box = await locator.boundingBox();

expect(box).not.toBeNull();

return box!.height;
};

test('should grow initially when ngModel has multiple lines', async ({ page }) => {
await page.goto('/E2eTextareaGrowBehavior');

Expand All @@ -38,10 +42,10 @@ test.describe('KbqTextareaModule', () => {
await page.goto('/E2eTextareaGrowBehavior');
const textarea = getTextarea(page);

await page.getByTestId('grow_set_short').click();
await pasteFromClipboard(page, textarea, 'test\ntest');
const shortHeight = await getHeight(textarea);

await page.getByTestId('grow_set_long').click();
await pasteFromClipboard(page, textarea, 'test\ntest\ntest\ntest\ntest\ntest');
const longHeight = await getHeight(textarea);

expect(longHeight).toBeGreaterThan(shortHeight);
Expand All @@ -51,16 +55,54 @@ test.describe('KbqTextareaModule', () => {
await page.goto('/E2eTextareaGrowBehavior');
const textarea = getTextarea(page);

await page.getByTestId('grow_set_long').click();
await pasteFromClipboard(page, textarea, 'test\ntest\ntest\ntest\ntest\ntest');
const longHeight = await getHeight(textarea);

await page.getByTestId('grow_set_short').click();
await pasteFromClipboard(page, textarea, 'test\ntest');
const shortHeight = await getHeight(textarea);

expect(shortHeight).toBeLessThan(longHeight);
});
});

test.describe('E2eTextareaGrowMaxRows', () => {
const getTextarea = (page: Page): Locator => page.getByTestId('grow-max-rows_textarea');

test('should grow initially when ngModel has multiple lines', async ({ page }) => {
await page.goto('/E2eTextareaGrowMaxRows');

expect(await getHeight(getTextarea(page))).toBeGreaterThan(50);
});

test('should grow to maxRows height when pasting text exceeding maxRows', async ({ page }) => {
await page.goto('/E2eTextareaGrowMaxRows');
const textarea = getTextarea(page);
const shortHeight = await getHeight(textarea);

await pasteFromClipboard(
page,
textarea,
'line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10'
);
expect(await getHeight(textarea)).toBeGreaterThan(shortHeight);
});

test('should not grow beyond maxRows height', async ({ page }) => {
await page.goto('/E2eTextareaGrowMaxRows');
const textarea = getTextarea(page);

await pasteFromClipboard(page, textarea, 'line1\nline2\nline3\nline4\nline5');
const atMaxRowsHeight = await getHeight(textarea);

await pasteFromClipboard(
page,
textarea,
'line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10'
);
expect(await getHeight(textarea)).toBe(atMaxRowsHeight);
Comment thread
artembelik marked this conversation as resolved.
});
});

test.describe('E2eTextareaScrollOnFocus', () => {
test('should not scroll the page when focusing a kbqTextarea', async ({ page }) => {
await page.goto('/E2eTextareaScrollOnFocus');
Expand Down
40 changes: 32 additions & 8 deletions packages/components/textarea/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,6 @@ export class E2eTextareaStates {
<kbq-form-field>
<textarea kbqTextarea data-testid="grow_textarea" [(ngModel)]="value"></textarea>
</kbq-form-field>

<button data-testid="grow_set_short" type="button" (click)="value.set(short)">Short</button>
<button data-testid="grow_set_medium" type="button" (click)="value.set(medium)">Medium</button>
<button data-testid="grow_set_long" type="button" (click)="value.set(long)">Long</button>
`,
styles: `
:host {
Expand All @@ -121,11 +117,39 @@ export class E2eTextareaStates {
}
})
export class E2eTextareaGrowBehavior {
protected readonly short = 'test\ntest';
protected readonly medium = 'test\ntest\ntest';
protected readonly long = 'test\ntest\ntest\ntest\ntest\ntest';
protected readonly value = model('test\ntest\ntest');
}

protected readonly value = model<string>(this.medium);
@Component({
selector: 'e2e-textarea-grow-max-rows',
imports: [KbqTextareaModule, FormsModule],
template: `
<kbq-form-field>
<textarea
kbqTextarea
data-testid="grow-max-rows_textarea"
[canGrow]="true"
[maxRows]="5"
[(ngModel)]="value"
></textarea>
</kbq-form-field>
`,
styles: `
:host {
display: flex;
flex-direction: column;
gap: var(--kbq-size-s);
padding: var(--kbq-size-s);
width: 320px;
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'data-testid': 'e2eTextareaGrowMaxRows'
}
})
export class E2eTextareaGrowMaxRows {
protected readonly value = model('line1\nline2\nline3');
}

const longTextareaContent =
Expand Down
37 changes: 37 additions & 0 deletions packages/components/textarea/textarea.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,19 @@ class TextareaControlWithAsyncValidators {
});
}

@Component({
imports: [KbqTextareaModule, FormsModule],
template: `
<kbq-form-field>
<textarea kbqTextarea [canGrow]="true" [maxRows]="3" [(ngModel)]="value"></textarea>
</kbq-form-field>
`
})
class KbqTextareaGrowWithMaxRows {
readonly textarea = viewChild.required(KbqTextarea);
value: string = 'line1\nline2';
}

@Component({
imports: [KbqTextareaModule, ReactiveFormsModule],
template: `
Expand Down Expand Up @@ -279,6 +292,30 @@ describe('KbqTextarea', () => {

expect(getTextareaElement(fixture).classList.contains('kbq-textarea-resizable')).toBe(false);
});

it('should not have kbq-textarea_max-row-limit-reached class when maxRows is not exceeded', () => {
const fixture = createComponent(KbqTextareaGrowWithMaxRows);

fixture.detectChanges();

expect(getTextareaElement(fixture).classList.contains('kbq-textarea_max-row-limit-reached')).toBe(false);
});
});

describe('grow behavior', () => {
it('should call stateChanges.next when input event fires', fakeAsync(() => {
const fixture = createComponent(KbqTextareaForBehaviors);

fixture.detectChanges();
tick();

const textarea = fixture.debugElement.query(By.directive(KbqTextarea)).injector.get(KbqTextarea);
const spy = jest.spyOn(textarea.stateChanges, 'next');

dispatchFakeEvent(getTextareaElement(fixture), 'input');

expect(spy).toHaveBeenCalled();
}));
});

describe('ErrorStateMatcher', () => {
Expand Down
16 changes: 6 additions & 10 deletions packages/components/textarea/textarea.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { coerceBooleanProperty, coerceCssPixelValue } from '@angular/cdk/coercion';
import { Platform } from '@angular/cdk/platform';
import {
booleanAttribute,
Expand All @@ -25,7 +25,6 @@ import {
} from '@koobiq/components/core';
import { KbqFormFieldControl } from '@koobiq/components/form-field';
import { Subject, Subscription } from 'rxjs';
import { delay } from 'rxjs/operators';

export const KBQ_TEXTAREA_VALUE_ACCESSOR = new InjectionToken<{ value: any }>('KBQ_TEXTAREA_VALUE_ACCESSOR');

Expand All @@ -45,7 +44,7 @@ let nextUniqueId = 0;
'[required]': 'required',
'(blur)': 'onBlur()',
'(focus)': 'focusChanged(true)',
'(paste)': 'stateChanges.next()'
'(input)': 'stateChanges.next()'
},
Comment thread
artembelik marked this conversation as resolved.
exportAs: 'kbqTextarea'
})
Expand Down Expand Up @@ -229,7 +228,7 @@ export class KbqTextarea
// eslint-disable-next-line @angular-eslint/no-lifecycle-call
this.parent?.animationDone.subscribe(() => this.ngOnInit());

this.growSubscription = this.stateChanges.pipe(delay(0)).subscribe(this.grow);
this.growSubscription = this.stateChanges.subscribe(this.grow);
}
Comment thread
artembelik marked this conversation as resolved.

ngOnInit() {
Expand Down Expand Up @@ -313,12 +312,9 @@ export class KbqTextarea

this.rowsCount = Math.floor(height / this.lineHeight);

if (!this.maxRowLimitReached) {
textarea.style.minHeight = `${height}px`;
} else if (!textarea.style.minHeight && this.lineHeight) {
// need for first initialization when value above maxRows
textarea.style.minHeight = `${this.maxRows() * this.lineHeight}px`;
}
textarea.style.minHeight = coerceCssPixelValue(
this.maxRowLimitReached ? this.maxRows() * this.lineHeight : height
Comment thread
artembelik marked this conversation as resolved.
);
});
};

Expand Down
8 changes: 7 additions & 1 deletion packages/e2e/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,12 @@ import {
E2eTagListStates,
E2eTagStateAndStyle
} from '../components/tags/e2e';
import { E2eTextareaGrowBehavior, E2eTextareaScrollOnFocus, E2eTextareaStates } from '../components/textarea/e2e';
import {
E2eTextareaGrowBehavior,
E2eTextareaGrowMaxRows,
E2eTextareaScrollOnFocus,
E2eTextareaStates
} from '../components/textarea/e2e';
import { E2eTimepickerStates } from '../components/timepicker/e2e';
import { E2eTimezoneStates } from '../components/timezone/e2e';
import { E2eToastStates } from '../components/toast/e2e';
Expand Down Expand Up @@ -135,6 +140,7 @@ const components = [
E2eAccordionStates,
E2eTextareaStates,
E2eTextareaGrowBehavior,
E2eTextareaGrowMaxRows,
E2eTextareaScrollOnFocus,
E2eDatepickerStates,
E2eTableStates,
Expand Down
3 changes: 2 additions & 1 deletion playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export default defineConfig({
deviceScaleFactor: 2,
reducedMotion: 'reduce',
viewport
}
},
permissions: ['clipboard-read', 'clipboard-write']
}
});
Loading