Skip to content

Commit c405e59

Browse files
authored
fix(ui): email field now correctly renders autocomplete attribute (#7322)
Adds test as well for the email field
1 parent a35979f commit c405e59

File tree

12 files changed

+611
-49
lines changed

12 files changed

+611
-49
lines changed

packages/ui/src/providers/ComponentMap/buildComponentMap/fields.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,7 @@ export const mapFields = (args: {
446446
const emailField: EmailFieldProps = {
447447
...baseFieldProps,
448448
name: field.name,
449+
autoComplete: field.admin?.autoComplete,
449450
className: field.admin?.className,
450451
disabled: field.admin?.disabled,
451452
placeholder: field.admin?.placeholder,
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use client'
2+
3+
import React from 'react'
4+
5+
export const AfterInput: React.FC = () => {
6+
return <label className="after-input">#after-input</label>
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use client'
2+
3+
import React from 'react'
4+
5+
export const BeforeInput: React.FC = () => {
6+
return <label className="before-input">#before-input</label>
7+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
'use client'
2+
3+
import { useField, useFormFields, useFormSubmitted } from '@payloadcms/ui'
4+
import React from 'react'
5+
6+
const CustomError: React.FC<any> = (props) => {
7+
const { path: pathFromProps } = props
8+
const submitted = useFormSubmitted()
9+
const { path } = useField(pathFromProps)
10+
const field = useFormFields(([fields]) => (fields && fields?.[path]) || null)
11+
const { valid } = field || {}
12+
13+
const showError = submitted && !valid
14+
15+
if (showError) {
16+
return <div className="custom-error">#custom-error</div>
17+
}
18+
19+
return null
20+
}
21+
22+
export default CustomError
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
'use client'
2+
3+
import { useFieldProps } from '@payloadcms/ui'
4+
import React from 'react'
5+
6+
const CustomLabel = ({ schemaPath }) => {
7+
const { path: pathFromContext } = useFieldProps()
8+
9+
const path = pathFromContext ?? schemaPath // pathFromContext will be undefined in list view
10+
11+
return (
12+
<label className="custom-label" htmlFor={`field-${path.replace(/\./g, '__')}`}>
13+
#label
14+
</label>
15+
)
16+
}
17+
18+
export default CustomLabel
Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
import type { Page } from '@playwright/test'
2+
3+
import { expect, test } from '@playwright/test'
4+
import path from 'path'
5+
import { wait } from 'payload/shared'
6+
import { fileURLToPath } from 'url'
7+
8+
import type { PayloadTestSDK } from '../../../helpers/sdk/index.js'
9+
import type { Config } from '../../payload-types.js'
10+
11+
import {
12+
ensureCompilationIsDone,
13+
exactText,
14+
initPageConsoleErrorCatch,
15+
saveDocAndAssert,
16+
} from '../../../helpers.js'
17+
import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
18+
import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js'
19+
import { reInitializeDB } from '../../../helpers/reInitializeDB.js'
20+
import { RESTClient } from '../../../helpers/rest.js'
21+
import { TEST_TIMEOUT_LONG } from '../../../playwright.config.js'
22+
import { emailFieldsSlug } from '../../slugs.js'
23+
import { anotherEmailDoc, emailDoc } from './shared.js'
24+
25+
const filename = fileURLToPath(import.meta.url)
26+
const currentFolder = path.dirname(filename)
27+
const dirname = path.resolve(currentFolder, '../../')
28+
29+
const { beforeAll, beforeEach, describe } = test
30+
31+
let payload: PayloadTestSDK<Config>
32+
let client: RESTClient
33+
let page: Page
34+
let serverURL: string
35+
// If we want to make this run in parallel: test.describe.configure({ mode: 'parallel' })
36+
let url: AdminUrlUtil
37+
38+
describe('Email', () => {
39+
beforeAll(async ({ browser }, testInfo) => {
40+
testInfo.setTimeout(TEST_TIMEOUT_LONG)
41+
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
42+
;({ payload, serverURL } = await initPayloadE2ENoConfig({
43+
dirname,
44+
// prebuild,
45+
}))
46+
url = new AdminUrlUtil(serverURL, emailFieldsSlug)
47+
48+
const context = await browser.newContext()
49+
page = await context.newPage()
50+
initPageConsoleErrorCatch(page)
51+
await reInitializeDB({
52+
serverURL,
53+
snapshotKey: 'fieldsEmailTest',
54+
uploadsDir: path.resolve(dirname, './collections/Upload/uploads'),
55+
})
56+
await ensureCompilationIsDone({ page, serverURL })
57+
})
58+
beforeEach(async () => {
59+
await reInitializeDB({
60+
serverURL,
61+
snapshotKey: 'fieldsEmailTest',
62+
uploadsDir: path.resolve(dirname, './collections/Upload/uploads'),
63+
})
64+
65+
if (client) {
66+
await client.logout()
67+
}
68+
client = new RESTClient(null, { defaultSlug: 'users', serverURL })
69+
await client.login()
70+
71+
await ensureCompilationIsDone({ page, serverURL })
72+
})
73+
74+
test('should display field in list view', async () => {
75+
await page.goto(url.list)
76+
const emailCell = page.locator('.row-1 .cell-email')
77+
await expect(emailCell).toHaveText(emailDoc.email)
78+
})
79+
80+
test('should hide field in column selector when admin.disableListColumn', async () => {
81+
await page.goto(url.list)
82+
await page.locator('.list-controls__toggle-columns').click()
83+
84+
await expect(page.locator('.column-selector')).toBeVisible()
85+
86+
// Check if "Disable List Column Text" is not present in the column options
87+
await expect(
88+
page.locator(`.column-selector .column-selector__column`, {
89+
hasText: exactText('Disable List Column Text'),
90+
}),
91+
).toBeHidden()
92+
})
93+
94+
test('should show field in filter when admin.disableListColumn is true', async () => {
95+
await page.goto(url.list)
96+
await page.locator('.list-controls__toggle-where').click()
97+
await page.locator('.where-builder__add-first-filter').click()
98+
99+
const initialField = page.locator('.condition__field')
100+
await initialField.click()
101+
102+
await expect(
103+
initialField.locator(`.rs__menu-list:has-text("Disable List Column Text")`),
104+
).toBeVisible()
105+
})
106+
107+
test('should display field in list view column selector if admin.disableListColumn is false and admin.disableListFilter is true', async () => {
108+
await page.goto(url.list)
109+
await page.locator('.list-controls__toggle-columns').click()
110+
111+
await expect(page.locator('.column-selector')).toBeVisible()
112+
113+
// Check if "Disable List Filter Text" is present in the column options
114+
await expect(
115+
page.locator(`.column-selector .column-selector__column`, {
116+
hasText: exactText('Disable List Filter Text'),
117+
}),
118+
).toBeVisible()
119+
})
120+
121+
test('should hide field in filter when admin.disableListFilter is true', async () => {
122+
await page.goto(url.list)
123+
await page.locator('.list-controls__toggle-where').click()
124+
await page.locator('.where-builder__add-first-filter').click()
125+
126+
const initialField = page.locator('.condition__field')
127+
await initialField.click()
128+
129+
await expect(
130+
initialField.locator(`.rs__option :has-text("Disable List Filter Text")`),
131+
).toBeHidden()
132+
})
133+
134+
test('should have autocomplete', async () => {
135+
await page.goto(url.create)
136+
const autoCompleteEmail = page.locator('#field-emailWithAutocomplete')
137+
await expect(autoCompleteEmail).toHaveAttribute('autocomplete')
138+
})
139+
140+
test('should show i18n label', async () => {
141+
await page.goto(url.create)
142+
143+
await expect(page.locator('label[for="field-i18nEmail"]')).toHaveText('Text en')
144+
})
145+
146+
test('should show i18n placeholder', async () => {
147+
await page.goto(url.create)
148+
await expect(page.locator('#field-i18nEmail')).toHaveAttribute('placeholder', 'en placeholder')
149+
})
150+
151+
test('should show i18n descriptions', async () => {
152+
await page.goto(url.create)
153+
const description = page.locator('.field-description-i18nEmail')
154+
await expect(description).toHaveText('en description')
155+
})
156+
157+
test('should render custom label', async () => {
158+
await page.goto(url.create)
159+
const label = page.locator('label.custom-label[for="field-customLabel"]')
160+
await expect(label).toHaveText('#label')
161+
})
162+
163+
test('should render custom error', async () => {
164+
await page.goto(url.create)
165+
const input = page.locator('input[id="field-customError"]')
166+
await input.fill('ab')
167+
await expect(input).toHaveValue('ab')
168+
const error = page.locator('.custom-error:near(input[id="field-customError"])')
169+
const submit = page.locator('button[type="button"][id="action-save"]')
170+
await submit.click()
171+
await expect(error).toHaveText('#custom-error')
172+
})
173+
174+
test('should render beforeInput and afterInput', async () => {
175+
await page.goto(url.create)
176+
const input = page.locator('input[id="field-beforeAndAfterInput"]')
177+
178+
const prevSibling = await input.evaluateHandle((el) => {
179+
return el.previousElementSibling
180+
})
181+
const prevSiblingText = await page.evaluate((el) => el.textContent, prevSibling)
182+
expect(prevSiblingText).toEqual('#before-input')
183+
184+
const nextSibling = await input.evaluateHandle((el) => {
185+
return el.nextElementSibling
186+
})
187+
const nextSiblingText = await page.evaluate((el) => el.textContent, nextSibling)
188+
expect(nextSiblingText).toEqual('#after-input')
189+
})
190+
191+
test('should reset filter conditions when adding additional filters', async () => {
192+
await page.goto(url.list)
193+
194+
// open the first filter options
195+
await page.locator('.list-controls__toggle-where').click()
196+
await expect(page.locator('.list-controls__where.rah-static--height-auto')).toBeVisible()
197+
await page.locator('.where-builder__add-first-filter').click()
198+
199+
const firstInitialField = page.locator('.condition__field')
200+
const firstOperatorField = page.locator('.condition__operator')
201+
const firstValueField = page.locator('.condition__value >> input')
202+
203+
await firstInitialField.click()
204+
const firstInitialFieldOptions = firstInitialField.locator('.rs__option')
205+
await firstInitialFieldOptions.locator('text=text').first().click()
206+
await expect(firstInitialField.locator('.rs__single-value')).toContainText('Text')
207+
208+
await firstOperatorField.click()
209+
await firstOperatorField.locator('.rs__option').locator('text=equals').click()
210+
211+
await firstValueField.fill('hello')
212+
213+
await wait(500)
214+
215+
await expect(firstValueField).toHaveValue('hello')
216+
217+
// open the second filter options
218+
await page.locator('.condition__actions-add').click()
219+
220+
const secondLi = page.locator('.where-builder__and-filters li:nth-child(2)')
221+
222+
await expect(secondLi).toBeVisible()
223+
224+
const secondInitialField = secondLi.locator('.condition__field')
225+
const secondOperatorField = secondLi.locator('.condition__operator >> input')
226+
const secondValueField = secondLi.locator('.condition__value >> input')
227+
228+
await expect(secondInitialField.locator('.rs__single-value')).toContainText('Email')
229+
await expect(secondOperatorField).toHaveValue('')
230+
await expect(secondValueField).toHaveValue('')
231+
})
232+
233+
test('should not re-render page upon typing in a value in the filter value field', async () => {
234+
await page.goto(url.list)
235+
236+
// open the first filter options
237+
await page.locator('.list-controls__toggle-where').click()
238+
await expect(page.locator('.list-controls__where.rah-static--height-auto')).toBeVisible()
239+
await page.locator('.where-builder__add-first-filter').click()
240+
241+
const firstInitialField = page.locator('.condition__field')
242+
const firstOperatorField = page.locator('.condition__operator')
243+
const firstValueField = page.locator('.condition__value >> input')
244+
245+
await firstInitialField.click()
246+
const firstInitialFieldOptions = firstInitialField.locator('.rs__option')
247+
await firstInitialFieldOptions.locator('text=text').first().click()
248+
await expect(firstInitialField.locator('.rs__single-value')).toContainText('Text')
249+
250+
await firstOperatorField.click()
251+
await firstOperatorField.locator('.rs__option').locator('text=equals').click()
252+
253+
// Type into the input field instead of filling it
254+
await firstValueField.click()
255+
await firstValueField.type('hello', { delay: 100 }) // Add a delay to simulate typing speed
256+
257+
// Wait for a short period to see if the input loses focus
258+
await page.waitForTimeout(500)
259+
260+
// Check if the input still has the correct value
261+
await expect(firstValueField).toHaveValue('hello')
262+
})
263+
264+
test('should still show second filter if two filters exist and first filter is removed', async () => {
265+
await page.goto(url.list)
266+
267+
// open the first filter options
268+
await page.locator('.list-controls__toggle-where').click()
269+
await expect(page.locator('.list-controls__where.rah-static--height-auto')).toBeVisible()
270+
await page.locator('.where-builder__add-first-filter').click()
271+
272+
const firstInitialField = page.locator('.condition__field')
273+
const firstOperatorField = page.locator('.condition__operator')
274+
const firstValueField = page.locator('.condition__value >> input')
275+
276+
await firstInitialField.click()
277+
const firstInitialFieldOptions = firstInitialField.locator('.rs__option')
278+
await firstInitialFieldOptions.locator('text=text').first().click()
279+
await expect(firstInitialField.locator('.rs__single-value')).toContainText('Text')
280+
281+
await firstOperatorField.click()
282+
await firstOperatorField.locator('.rs__option').locator('text=equals').click()
283+
284+
await firstValueField.fill('hello')
285+
286+
await wait(500)
287+
288+
await expect(firstValueField).toHaveValue('hello')
289+
290+
// open the second filter options
291+
await page.locator('.condition__actions-add').click()
292+
293+
const secondLi = page.locator('.where-builder__and-filters li:nth-child(2)')
294+
295+
await expect(secondLi).toBeVisible()
296+
297+
const secondInitialField = secondLi.locator('.condition__field')
298+
const secondOperatorField = secondLi.locator('.condition__operator')
299+
const secondValueField = secondLi.locator('.condition__value >> input')
300+
301+
await secondInitialField.click()
302+
const secondInitialFieldOptions = secondInitialField.locator('.rs__option')
303+
await secondInitialFieldOptions.locator('text=text').first().click()
304+
await expect(secondInitialField.locator('.rs__single-value')).toContainText('Text')
305+
306+
await secondOperatorField.click()
307+
await secondOperatorField.locator('.rs__option').locator('text=equals').click()
308+
309+
await secondValueField.fill('world')
310+
await expect(secondValueField).toHaveValue('world')
311+
312+
await wait(500)
313+
314+
const firstLi = page.locator('.where-builder__and-filters li:nth-child(1)')
315+
const removeButton = firstLi.locator('.condition__actions-remove')
316+
317+
// remove first filter
318+
await removeButton.click()
319+
320+
const filterListItems = page.locator('.where-builder__and-filters li')
321+
await expect(filterListItems).toHaveCount(1)
322+
323+
await expect(firstValueField).toHaveValue('world')
324+
})
325+
})

0 commit comments

Comments
 (0)