From 1089f5b1aba92863fdeefecb81bd6b1b0d8341be Mon Sep 17 00:00:00 2001 From: swimer11 <65334157+swimer11@users.noreply.github.com> Date: Fri, 6 Dec 2024 17:21:59 -0800 Subject: [PATCH 1/4] feat(select): add helper and error text --- core/src/components.d.ts | 16 ++ core/src/components/select/select.scss | 28 ++ core/src/components/select/select.tsx | 71 +++++ .../select/test/bottom-content/index.html | 81 ++++++ .../select/test/bottom-content/input.e2e.ts | 242 ++++++++++++++++++ packages/angular/src/directives/proxies.ts | 4 +- packages/vue/src/proxies.ts | 2 + 7 files changed, 442 insertions(+), 2 deletions(-) create mode 100644 core/src/components/select/test/bottom-content/index.html create mode 100644 core/src/components/select/test/bottom-content/input.e2e.ts diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 1bdfaa88545..a77c651c8ad 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -2755,6 +2755,10 @@ export namespace Components { * If `true`, the user cannot interact with the select. */ "disabled": boolean; + /** + * Text that is placed under the select and displayed when an error is detected. + */ + "errorText"?: string; /** * The toggle icon to show when the select is open. If defined, the icon rotation behavior in `md` mode will be disabled. If undefined, `toggleIcon` will be used for when the select is both open and closed. */ @@ -2763,6 +2767,10 @@ export namespace Components { * The fill for the item. If `"solid"` the item will have a background. If `"outline"` the item will be transparent with a border. Only available in `md` mode. */ "fill"?: 'outline' | 'solid'; + /** + * Text that is placed under the select and displayed when no error is detected. + */ + "helperText"?: string; /** * The interface the select should use: `action-sheet`, `popover`, `alert`, or `modal`. */ @@ -7568,6 +7576,10 @@ declare namespace LocalJSX { * If `true`, the user cannot interact with the select. */ "disabled"?: boolean; + /** + * Text that is placed under the select and displayed when an error is detected. + */ + "errorText"?: string; /** * The toggle icon to show when the select is open. If defined, the icon rotation behavior in `md` mode will be disabled. If undefined, `toggleIcon` will be used for when the select is both open and closed. */ @@ -7576,6 +7588,10 @@ declare namespace LocalJSX { * The fill for the item. If `"solid"` the item will have a background. If `"outline"` the item will be transparent with a border. Only available in `md` mode. */ "fill"?: 'outline' | 'solid'; + /** + * Text that is placed under the select and displayed when no error is detected. + */ + "helperText"?: string; /** * The interface the select should use: `action-sheet`, `popover`, `alert`, or `modal`. */ diff --git a/core/src/components/select/select.scss b/core/src/components/select/select.scss index 8b12f01ec1c..a9f9758869a 100644 --- a/core/src/components/select/select.scss +++ b/core/src/components/select/select.scss @@ -340,6 +340,34 @@ button { display: none; } +// Select Hint Text +// ---------------------------------------------------------------- + +/** + * Error text should only be shown when .ion-invalid is + * present on the input. Otherwise the helper text should + * be shown. + */ + .input-bottom .error-text { + display: none; + + color: var(--highlight-color-invalid); +} + +.input-bottom .helper-text { + display: block; + + color: #{$text-color-step-450}; +} + +:host(.ion-touched.ion-invalid) .input-bottom .error-text { + display: block; +} + +:host(.ion-touched.ion-invalid) .input-bottom .helper-text { + display: none; +} + // Select Native Wrapper // ---------------------------------------------------------------- diff --git a/core/src/components/select/select.tsx b/core/src/components/select/select.tsx index 3b4ef84f26f..d486cbf0c21 100644 --- a/core/src/components/select/select.tsx +++ b/core/src/components/select/select.tsx @@ -52,12 +52,15 @@ import type { SelectChangeEventDetail, SelectInterface, SelectCompareFn } from ' }) export class Select implements ComponentInterface { private inputId = `ion-sel-${selectIds++}`; + private helperTextId = `${this.inputId}-helper-text`; + private errorTextId = `${this.inputId}-error-text`; private overlay?: OverlaySelect; private focusEl?: HTMLButtonElement; private mutationO?: MutationObserver; private inheritedAttributes: Attributes = {}; private nativeWrapperEl: HTMLElement | undefined; private notchSpacerEl: HTMLElement | undefined; + private notchController?: NotchController; @@ -98,6 +101,16 @@ export class Select implements ComponentInterface { */ @Prop() fill?: 'outline' | 'solid'; + /** + * Text that is placed under the select and displayed when no error is detected. + */ + @Prop() helperText?: string; + + /** + * Text that is placed under the select and displayed when an error is detected. + */ + @Prop() errorText?: string; + /** * The interface the select should use: `action-sheet`, `popover`, `alert`, or `modal`. */ @@ -714,6 +727,36 @@ export class Select implements ComponentInterface { return this.getText() !== ''; } + /** + * Renders the helper text or error text values + */ + private renderHintText() { + const { helperText, errorText, helperTextId, errorTextId } = this; + + return [ +
+ {helperText} +
, +
+ {errorText} +
, + ]; + } + + private getHintTextID(): string | undefined { + const { el, helperText, errorText, helperTextId, errorTextId } = this; + + if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) { + return errorTextId; + } + + if (helperText) { + return helperTextId; + } + + return undefined; + } + private get childOpts() { return Array.from(this.el.querySelectorAll('ion-select-option')); } @@ -812,6 +855,33 @@ export class Select implements ComponentInterface { this.ionBlur.emit(); }; + /** + * Responsible for rendering helper text and + * error text. This element should only + * be rendered if hint text is set. + */ + private renderBottomContent() { + const { helperText, errorText } = this; + + /** + * undefined and empty string values should + * be treated as not having helper/error text. + */ + const hasHintText = !!helperText || !!errorText; + console.log(`HelperText: ${helperText}`); + console.log(`errorText: ${errorText}`); + if (!hasHintText) { + console.log("No text"); + return; + } + + return ( +
+ {this.renderHintText()} +
+ ); + } + private renderLabel() { const { label } = this; @@ -1069,6 +1139,7 @@ export class Select implements ComponentInterface { {hasFloatingOrStackedLabel && this.renderSelectIcon()} {shouldRenderHighlight &&
} + {this.renderBottomContent()} ); } diff --git a/core/src/components/select/test/bottom-content/index.html b/core/src/components/select/test/bottom-content/index.html new file mode 100644 index 00000000000..1a5bca16cf2 --- /dev/null +++ b/core/src/components/select/test/bottom-content/index.html @@ -0,0 +1,81 @@ + + + + + Input - Bottom Content + + + + + + + + + + + + + + select - Bottom Content + + + + +
+ +
+

Select with Helper

+ +
Favorite Fruit (Required)
+ Apple + Bananna +
+
+ +
+

Counter with Error

+ +
+
+ + +
+
+ + diff --git a/core/src/components/select/test/bottom-content/input.e2e.ts b/core/src/components/select/test/bottom-content/input.e2e.ts new file mode 100644 index 00000000000..5de0ca79e19 --- /dev/null +++ b/core/src/components/select/test/bottom-content/input.e2e.ts @@ -0,0 +1,242 @@ +import { expect } from '@playwright/test'; +import { configs, test } from '@utils/test/playwright'; + +configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => { + test.describe(title('input: bottom content'), () => { + test('entire input component should render correctly with no fill', async ({ page }) => { + await page.setContent( + ` + + `, + config + ); + const input = page.locator('ion-input'); + await expect(input).toHaveScreenshot(screenshot(`input-full-bottom-no-fill`)); + }); + test('entire input component should render correctly with solid fill', async ({ page }) => { + await page.setContent( + ` + + `, + config + ); + const input = page.locator('ion-input'); + await expect(input).toHaveScreenshot(screenshot(`input-full-bottom-solid`)); + }); + test('entire input component should render correctly with outline fill', async ({ page }) => { + await page.setContent( + ` + + `, + config + ); + const input = page.locator('ion-input'); + await expect(input).toHaveScreenshot(screenshot(`input-full-bottom-outline`)); + }); + }); +}); + +/** + * Rendering is the same across modes + */ +configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => { + test.describe(title('input: bottom content functionality'), () => { + test('should not render bottom content if no hint or counter is enabled', async ({ page }) => { + await page.setContent(``, config); + + const bottomEl = page.locator('ion-input .input-bottom'); + await expect(bottomEl).toHaveCount(0); + }); + }); +}); + +/** + * Rendering is the same across modes + */ +configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => { + test.describe(title('input: hint text'), () => { + test.describe('input: hint text functionality', () => { + test('helper text should be visible initially', async ({ page }) => { + await page.setContent( + ``, + config + ); + + const helperText = page.locator('ion-input .helper-text'); + const errorText = page.locator('ion-input .error-text'); + await expect(helperText).toBeVisible(); + await expect(helperText).toHaveText('my helper'); + await expect(errorText).toBeHidden(); + }); + test('input should have an aria-describedby attribute when helper text is present', async ({ page }) => { + await page.setContent( + ``, + config + ); + + const input = page.locator('ion-input input'); + const helperText = page.locator('ion-input .helper-text'); + const helperTextId = await helperText.getAttribute('id'); + const ariaDescribedBy = await input.getAttribute('aria-describedby'); + + expect(ariaDescribedBy).toBe(helperTextId); + }); + test('error text should be visible when input is invalid', async ({ page }) => { + await page.setContent( + ``, + config + ); + + const helperText = page.locator('ion-input .helper-text'); + const errorText = page.locator('ion-input .error-text'); + await expect(helperText).toBeHidden(); + await expect(errorText).toBeVisible(); + await expect(errorText).toHaveText('my error'); + }); + test('error text should change when variable is customized', async ({ page }) => { + await page.setContent( + ` + + + `, + config + ); + + const errorText = page.locator('ion-input .error-text'); + await expect(errorText).toHaveScreenshot(screenshot(`input-error-custom-color`)); + }); + test('input should have an aria-describedby attribute when error text is present', async ({ page }) => { + await page.setContent( + ``, + config + ); + + const input = page.locator('ion-input input'); + const errorText = page.locator('ion-input .error-text'); + const errorTextId = await errorText.getAttribute('id'); + const ariaDescribedBy = await input.getAttribute('aria-describedby'); + + expect(ariaDescribedBy).toBe(errorTextId); + }); + test('input should have aria-invalid attribute when input is invalid', async ({ page }) => { + await page.setContent( + ``, + config + ); + + const input = page.locator('ion-input input'); + + await expect(input).toHaveAttribute('aria-invalid'); + }); + test('input should not have aria-invalid attribute when input is valid', async ({ page }) => { + await page.setContent( + ``, + config + ); + + const input = page.locator('ion-input input'); + + await expect(input).not.toHaveAttribute('aria-invalid'); + }); + test('input should not have aria-describedby attribute when no hint or error text is present', async ({ + page, + }) => { + await page.setContent(``, config); + + const input = page.locator('ion-input input'); + + await expect(input).not.toHaveAttribute('aria-describedby'); + }); + }); + test.describe('input: hint text rendering', () => { + test.describe('regular inputs', () => { + test('should not have visual regressions when rendering helper text', async ({ page }) => { + await page.setContent(``, config); + + const bottomEl = page.locator('ion-input .input-bottom'); + await expect(bottomEl).toHaveScreenshot(screenshot(`input-bottom-content-helper`)); + }); + test('should not have visual regressions when rendering error text', async ({ page }) => { + await page.setContent( + ``, + config + ); + + const bottomEl = page.locator('ion-input .input-bottom'); + await expect(bottomEl).toHaveScreenshot(screenshot(`input-bottom-content-error`)); + }); + }); + }); + }); +}); + +/** + * Rendering is the same across modes + */ +configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => { + test.describe(title('input: counter'), () => { + test.describe('input: counter functionality', () => { + test('should not activate if maxlength is not specified even if bottom content is visible', async ({ page }) => { + await page.setContent( + ` + + `, + config + ); + const itemCounter = page.locator('ion-input .counter'); + await expect(itemCounter).toBeHidden(); + }); + test('default formatter should be used', async ({ page }) => { + await page.setContent( + ` + + `, + config + ); + const itemCounter = page.locator('ion-input .counter'); + expect(await itemCounter.textContent()).toBe('0 / 20'); + }); + test('custom formatter should be used when provided', async ({ page }) => { + await page.setContent( + ` + + + + `, + config + ); + + const input = page.locator('ion-input input'); + const itemCounter = page.locator('ion-input .counter'); + expect(await itemCounter.textContent()).toBe('20 characters left'); + + await input.click(); + await input.type('abcde'); + + await page.waitForChanges(); + + expect(await itemCounter.textContent()).toBe('15 characters left'); + }); + }); + test.describe('input: counter rendering', () => { + test.describe('regular inputs', () => { + test('should not have visual regressions when rendering counter', async ({ page }) => { + await page.setContent(``, config); + + const bottomEl = page.locator('ion-input .input-bottom'); + await expect(bottomEl).toHaveScreenshot(screenshot(`input-bottom-content-counter`)); + }); + }); + }); + }); +}); diff --git a/packages/angular/src/directives/proxies.ts b/packages/angular/src/directives/proxies.ts index 675c37bd1c1..09e05390a90 100644 --- a/packages/angular/src/directives/proxies.ts +++ b/packages/angular/src/directives/proxies.ts @@ -2060,7 +2060,7 @@ export declare interface IonSegmentView extends Components.IonSegmentView { @ProxyCmp({ - inputs: ['cancelText', 'color', 'compareWith', 'disabled', 'expandedIcon', 'fill', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'selectedText', 'shape', 'toggleIcon', 'value'], + inputs: ['cancelText', 'color', 'compareWith', 'disabled', 'errorText', 'expandedIcon', 'fill', 'helperText', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'selectedText', 'shape', 'toggleIcon', 'value'], methods: ['open'] }) @Component({ @@ -2068,7 +2068,7 @@ export declare interface IonSegmentView extends Components.IonSegmentView { changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['cancelText', 'color', 'compareWith', 'disabled', 'expandedIcon', 'fill', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'selectedText', 'shape', 'toggleIcon', 'value'], + inputs: ['cancelText', 'color', 'compareWith', 'disabled', 'errorText', 'expandedIcon', 'fill', 'helperText', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'selectedText', 'shape', 'toggleIcon', 'value'], }) export class IonSelect { protected el: HTMLElement; diff --git a/packages/vue/src/proxies.ts b/packages/vue/src/proxies.ts index 7dd5812ebbc..079a64fd710 100644 --- a/packages/vue/src/proxies.ts +++ b/packages/vue/src/proxies.ts @@ -771,6 +771,8 @@ export const IonSelect = /*@__PURE__*/ defineContainer Date: Fri, 6 Dec 2024 22:35:22 -0800 Subject: [PATCH 2/4] feat(select): adds more testing --- .../select/test/bottom-content/index.html | 44 ++++++++++++++----- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/core/src/components/select/test/bottom-content/index.html b/core/src/components/select/test/bottom-content/index.html index 1a5bca16cf2..49caf4346fa 100644 --- a/core/src/components/select/test/bottom-content/index.html +++ b/core/src/components/select/test/bottom-content/index.html @@ -52,24 +52,44 @@
-

Select with Helper

- -
Favorite Fruit (Required)
+

Select with Helper text

+ +
Favorite Fruit
Apple Bananna
-

Counter with Error

- +

Select with Error text

+ +
Favorite Fruit
+ Apple + Bananna +
+
+ +
+

Select with Helper and error text| Valid

+ +
Favorite Fruit
+ Apple + Bananna +
+
+ +
+

Select with no helper or error text

+ +
Favorite Fruit
+ Apple + Bananna +
From 20a40ca8850f7497978be2cc0f44562d3a74fd8e Mon Sep 17 00:00:00 2001 From: Tyler Cohade Date: Fri, 6 Dec 2024 22:48:54 -0800 Subject: [PATCH 3/4] feat(select): add helper and error text to core --- core/src/components/select/select.tsx | 35 ++++--------------- .../select/test/bottom-content/index.html | 22 ++++++------ 2 files changed, 16 insertions(+), 41 deletions(-) diff --git a/core/src/components/select/select.tsx b/core/src/components/select/select.tsx index d486cbf0c21..818735f990d 100644 --- a/core/src/components/select/select.tsx +++ b/core/src/components/select/select.tsx @@ -60,7 +60,6 @@ export class Select implements ComponentInterface { private inheritedAttributes: Attributes = {}; private nativeWrapperEl: HTMLElement | undefined; private notchSpacerEl: HTMLElement | undefined; - private notchController?: NotchController; @@ -109,7 +108,7 @@ export class Select implements ComponentInterface { /** * Text that is placed under the select and displayed when an error is detected. */ - @Prop() errorText?: string; + @Prop() errorText?: string; /** * The interface the select should use: `action-sheet`, `popover`, `alert`, or `modal`. @@ -742,21 +741,6 @@ export class Select implements ComponentInterface { , ]; } - - private getHintTextID(): string | undefined { - const { el, helperText, errorText, helperTextId, errorTextId } = this; - - if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) { - return errorTextId; - } - - if (helperText) { - return helperTextId; - } - - return undefined; - } - private get childOpts() { return Array.from(this.el.querySelectorAll('ion-select-option')); } @@ -856,10 +840,10 @@ export class Select implements ComponentInterface { }; /** - * Responsible for rendering helper text and - * error text. This element should only - * be rendered if hint text is set. - */ + * Responsible for rendering helper text and + * error text. This element should only + * be rendered if hint text is set. + */ private renderBottomContent() { const { helperText, errorText } = this; @@ -868,18 +852,11 @@ export class Select implements ComponentInterface { * be treated as not having helper/error text. */ const hasHintText = !!helperText || !!errorText; - console.log(`HelperText: ${helperText}`); - console.log(`errorText: ${errorText}`); if (!hasHintText) { - console.log("No text"); return; } - return ( -
- {this.renderHintText()} -
- ); + return
{this.renderHintText()}
; } private renderLabel() { diff --git a/core/src/components/select/test/bottom-content/index.html b/core/src/components/select/test/bottom-content/index.html index 49caf4346fa..7c694f501a2 100644 --- a/core/src/components/select/test/bottom-content/index.html +++ b/core/src/components/select/test/bottom-content/index.html @@ -50,12 +50,9 @@
-

Select with Helper text

- +
Favorite Fruit
Apple Bananna @@ -64,8 +61,7 @@

Select with Helper text

Select with Error text

- +
Favorite Fruit
Apple Bananna @@ -74,8 +70,12 @@

Select with Error text

Select with Helper and error text| Valid

- +
Favorite Fruit
Apple Bananna @@ -84,8 +84,7 @@

Select with Helper and error text| Valid

Select with no helper or error text

- +
Favorite Fruit
Apple Bananna @@ -93,8 +92,7 @@

Select with no helper or error text

- + From 3aabc619f42cc1bf4241ca26dfe84db6a080fb1b Mon Sep 17 00:00:00 2001 From: Tyler Cohade Date: Fri, 6 Dec 2024 23:05:59 -0800 Subject: [PATCH 4/4] update testing for bottom-content --- core/api.txt | 2 + .../select/test/bottom-content/input.e2e.ts | 242 ------------------ .../select/test/bottom-content/select.e2e.ts | 41 +++ 3 files changed, 43 insertions(+), 242 deletions(-) delete mode 100644 core/src/components/select/test/bottom-content/input.e2e.ts create mode 100644 core/src/components/select/test/bottom-content/select.e2e.ts diff --git a/core/api.txt b/core/api.txt index 67d49417556..c589bdeaca1 100644 --- a/core/api.txt +++ b/core/api.txt @@ -1619,8 +1619,10 @@ ion-select,prop,cancelText,string,'Cancel',false,false ion-select,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record | undefined,undefined,false,true ion-select,prop,compareWith,((currentValue: any, compareValue: any) => boolean) | null | string | undefined,undefined,false,false ion-select,prop,disabled,boolean,false,false,false +ion-select,prop,errorText,string | undefined,undefined,false,false ion-select,prop,expandedIcon,string | undefined,undefined,false,false ion-select,prop,fill,"outline" | "solid" | undefined,undefined,false,false +ion-select,prop,helperText,string | undefined,undefined,false,false ion-select,prop,interface,"action-sheet" | "alert" | "modal" | "popover",'alert',false,false ion-select,prop,interfaceOptions,any,{},false,false ion-select,prop,justify,"end" | "space-between" | "start" | undefined,undefined,false,false diff --git a/core/src/components/select/test/bottom-content/input.e2e.ts b/core/src/components/select/test/bottom-content/input.e2e.ts deleted file mode 100644 index 5de0ca79e19..00000000000 --- a/core/src/components/select/test/bottom-content/input.e2e.ts +++ /dev/null @@ -1,242 +0,0 @@ -import { expect } from '@playwright/test'; -import { configs, test } from '@utils/test/playwright'; - -configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => { - test.describe(title('input: bottom content'), () => { - test('entire input component should render correctly with no fill', async ({ page }) => { - await page.setContent( - ` - - `, - config - ); - const input = page.locator('ion-input'); - await expect(input).toHaveScreenshot(screenshot(`input-full-bottom-no-fill`)); - }); - test('entire input component should render correctly with solid fill', async ({ page }) => { - await page.setContent( - ` - - `, - config - ); - const input = page.locator('ion-input'); - await expect(input).toHaveScreenshot(screenshot(`input-full-bottom-solid`)); - }); - test('entire input component should render correctly with outline fill', async ({ page }) => { - await page.setContent( - ` - - `, - config - ); - const input = page.locator('ion-input'); - await expect(input).toHaveScreenshot(screenshot(`input-full-bottom-outline`)); - }); - }); -}); - -/** - * Rendering is the same across modes - */ -configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => { - test.describe(title('input: bottom content functionality'), () => { - test('should not render bottom content if no hint or counter is enabled', async ({ page }) => { - await page.setContent(``, config); - - const bottomEl = page.locator('ion-input .input-bottom'); - await expect(bottomEl).toHaveCount(0); - }); - }); -}); - -/** - * Rendering is the same across modes - */ -configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => { - test.describe(title('input: hint text'), () => { - test.describe('input: hint text functionality', () => { - test('helper text should be visible initially', async ({ page }) => { - await page.setContent( - ``, - config - ); - - const helperText = page.locator('ion-input .helper-text'); - const errorText = page.locator('ion-input .error-text'); - await expect(helperText).toBeVisible(); - await expect(helperText).toHaveText('my helper'); - await expect(errorText).toBeHidden(); - }); - test('input should have an aria-describedby attribute when helper text is present', async ({ page }) => { - await page.setContent( - ``, - config - ); - - const input = page.locator('ion-input input'); - const helperText = page.locator('ion-input .helper-text'); - const helperTextId = await helperText.getAttribute('id'); - const ariaDescribedBy = await input.getAttribute('aria-describedby'); - - expect(ariaDescribedBy).toBe(helperTextId); - }); - test('error text should be visible when input is invalid', async ({ page }) => { - await page.setContent( - ``, - config - ); - - const helperText = page.locator('ion-input .helper-text'); - const errorText = page.locator('ion-input .error-text'); - await expect(helperText).toBeHidden(); - await expect(errorText).toBeVisible(); - await expect(errorText).toHaveText('my error'); - }); - test('error text should change when variable is customized', async ({ page }) => { - await page.setContent( - ` - - - `, - config - ); - - const errorText = page.locator('ion-input .error-text'); - await expect(errorText).toHaveScreenshot(screenshot(`input-error-custom-color`)); - }); - test('input should have an aria-describedby attribute when error text is present', async ({ page }) => { - await page.setContent( - ``, - config - ); - - const input = page.locator('ion-input input'); - const errorText = page.locator('ion-input .error-text'); - const errorTextId = await errorText.getAttribute('id'); - const ariaDescribedBy = await input.getAttribute('aria-describedby'); - - expect(ariaDescribedBy).toBe(errorTextId); - }); - test('input should have aria-invalid attribute when input is invalid', async ({ page }) => { - await page.setContent( - ``, - config - ); - - const input = page.locator('ion-input input'); - - await expect(input).toHaveAttribute('aria-invalid'); - }); - test('input should not have aria-invalid attribute when input is valid', async ({ page }) => { - await page.setContent( - ``, - config - ); - - const input = page.locator('ion-input input'); - - await expect(input).not.toHaveAttribute('aria-invalid'); - }); - test('input should not have aria-describedby attribute when no hint or error text is present', async ({ - page, - }) => { - await page.setContent(``, config); - - const input = page.locator('ion-input input'); - - await expect(input).not.toHaveAttribute('aria-describedby'); - }); - }); - test.describe('input: hint text rendering', () => { - test.describe('regular inputs', () => { - test('should not have visual regressions when rendering helper text', async ({ page }) => { - await page.setContent(``, config); - - const bottomEl = page.locator('ion-input .input-bottom'); - await expect(bottomEl).toHaveScreenshot(screenshot(`input-bottom-content-helper`)); - }); - test('should not have visual regressions when rendering error text', async ({ page }) => { - await page.setContent( - ``, - config - ); - - const bottomEl = page.locator('ion-input .input-bottom'); - await expect(bottomEl).toHaveScreenshot(screenshot(`input-bottom-content-error`)); - }); - }); - }); - }); -}); - -/** - * Rendering is the same across modes - */ -configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => { - test.describe(title('input: counter'), () => { - test.describe('input: counter functionality', () => { - test('should not activate if maxlength is not specified even if bottom content is visible', async ({ page }) => { - await page.setContent( - ` - - `, - config - ); - const itemCounter = page.locator('ion-input .counter'); - await expect(itemCounter).toBeHidden(); - }); - test('default formatter should be used', async ({ page }) => { - await page.setContent( - ` - - `, - config - ); - const itemCounter = page.locator('ion-input .counter'); - expect(await itemCounter.textContent()).toBe('0 / 20'); - }); - test('custom formatter should be used when provided', async ({ page }) => { - await page.setContent( - ` - - - - `, - config - ); - - const input = page.locator('ion-input input'); - const itemCounter = page.locator('ion-input .counter'); - expect(await itemCounter.textContent()).toBe('20 characters left'); - - await input.click(); - await input.type('abcde'); - - await page.waitForChanges(); - - expect(await itemCounter.textContent()).toBe('15 characters left'); - }); - }); - test.describe('input: counter rendering', () => { - test.describe('regular inputs', () => { - test('should not have visual regressions when rendering counter', async ({ page }) => { - await page.setContent(``, config); - - const bottomEl = page.locator('ion-input .input-bottom'); - await expect(bottomEl).toHaveScreenshot(screenshot(`input-bottom-content-counter`)); - }); - }); - }); - }); -}); diff --git a/core/src/components/select/test/bottom-content/select.e2e.ts b/core/src/components/select/test/bottom-content/select.e2e.ts new file mode 100644 index 00000000000..61e7d9528b9 --- /dev/null +++ b/core/src/components/select/test/bottom-content/select.e2e.ts @@ -0,0 +1,41 @@ +import { expect } from '@playwright/test'; +import { configs, test } from '@utils/test/playwright'; + +configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => { + test.describe(title('Select: Helper and Error Text'), () => { + test.describe('Select with helper text', () => { + test('should set label and show helper text', async ({ page }) => { + await page.setContent( + ` + +
Favorite Fruit
+ Apple + Bananna +
+ `, + config + ); + + const select = page.locator('ion-select'); + await expect(select).toHaveScreenshot(screenshot(`select-helper-text`)); + }); + }); + test.describe('Select with Error text', () => { + test('should set label and show error text', async ({ page }) => { + await page.setContent( + ` + +
Favorite Fruit
+ Apple + Bananna +
+ `, + config + ); + + const select = page.locator('ion-select'); + await expect(select).toHaveScreenshot(screenshot(`select-error-text`)); + }); + }); + }); +});