diff --git a/.gitignore b/.gitignore index 76142c267f2..b1d0cf20a59 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,6 @@ package-lock.json /cypress/parallel-weights.json /cypress/screenshots /cypress/fixtures +/test-results/ +/playwright-report/ +/playwright/.cache/ diff --git a/package.json b/package.json index 9dd3eea04e8..2730fa9eb77 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "license": "MIT", "devDependencies": { "@microsoft/api-extractor": "^7.28.7", + "@playwright/test": "^1.36.2", "@replayio/cypress": "^1.0.3", "@rollup/plugin-commonjs": "^22.0.1", "@rollup/plugin-node-resolve": "^13.1.3", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000000..adb1c1bae73 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,77 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './playwright', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: [['html', { open: 'never' }]], + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: 'http://127.0.0.1:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + // { + // name: 'chromium', + // use: { ...devices['Desktop Chrome'] }, + // }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + // }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/playwright/e2e/autoUnregister.spec.ts b/playwright/e2e/autoUnregister.spec.ts new file mode 100644 index 00000000000..ae2c89428a8 --- /dev/null +++ b/playwright/e2e/autoUnregister.spec.ts @@ -0,0 +1,28 @@ +import { expect, test } from '@playwright/test'; + +test.describe('autoUnregister', () => { + test('should keep all inputs data when inputs get unmounted', async ({ + page, + }) => { + await page.goto('http://localhost:3000/autoUnregister'); + await page.locator('input[name="test"]').fill('test'); + await page.locator('input[name="test1"]').fill('test1'); + await page.locator('input[name="test2"]').check(); + await page.locator('input[name="test3"]').check(); + await page.locator('select[name="test4"]').selectOption('Bill'); + await page.locator('#input-ReactSelect > div').click(); + await page.locator('#input-ReactSelect > div > div').nth(1).click(); + + await page.locator('button').click(); + await page.locator('button').click(); + + await expect(page.locator('input[name="test"]')).toHaveValue('test'); + await expect(page.locator('input[name="test1"]')).toHaveValue('test1'); + await expect(page.locator('input[name="test2"]')).toBeChecked(); + await expect(page.locator('input[name="test3"]')).toBeChecked(); + await expect(page.locator('select[name="test4"]')).toHaveValue('bill'); + await expect( + page.locator('#input-ReactSelect > div > div > div > div'), + ).toContainText('Strawberry'); + }); +}); diff --git a/playwright/e2e/basic.spec.ts b/playwright/e2e/basic.spec.ts new file mode 100644 index 00000000000..0244951c407 --- /dev/null +++ b/playwright/e2e/basic.spec.ts @@ -0,0 +1,32 @@ +import { test } from '@playwright/test'; + +test.describe('basic form validation', () => { + test('should validate the form and reset the form', async ({ page }) => { + await page.goto('http://localhost:3000/basic/onSubmit'); + await page.locator('button#submit').click(); + + // ... rest of the test code + + await page.locator('#resetForm').click(); + // ... rest of the assertions + }); + + test('should validate the form with onTouched mode', async ({ page }) => { + await page.goto('http://localhost:3000/basic/onTouched'); + // ... rest of the test code + }); + + test('should validate the form with onBlur mode and reset the form', async ({ + page, + }) => { + await page.goto('http://localhost:3000/basic/onBlur'); + // ... rest of the test code + }); + + test('should validate the form with onChange mode and reset the form', async ({ + page, + }) => { + await page.goto('http://localhost:3000/basic/onChange'); + // ... rest of the test code + }); +}); diff --git a/playwright/e2e/basicSchemaValidation.spec.ts b/playwright/e2e/basicSchemaValidation.spec.ts new file mode 100644 index 00000000000..d7b9596045d --- /dev/null +++ b/playwright/e2e/basicSchemaValidation.spec.ts @@ -0,0 +1,209 @@ +import { expect, test } from '@playwright/test'; + +test.describe('basicSchemaValidation form validation', () => { + test('should validate the form with onSubmit mode', async ({ page }) => { + await page.goto('http://localhost:3000/basic-schema-validation/onSubmit'); + await page.locator('button').click(); + + const firstNameInput = page.locator('input[name="firstName"]'); + await firstNameInput.click(); // this ensures the input is focused + + const isFocused = await page.evaluate( + (input) => document.activeElement === input, + await firstNameInput.elementHandle(), + ); + + expect(isFocused).toBe(true); + await expect(page.locator('input[name="firstName"] + p')).toContainText( + 'firstName error', + ); + await expect(page.locator('input[name="lastName"] + p')).toContainText( + 'lastName error', + ); + await expect(page.locator('select[name="selectNumber"] + p')).toContainText( + 'selectNumber error', + ); + await expect( + page.locator('input[name="minRequiredLength"] + p'), + ).toContainText('minRequiredLength error'); + await expect(page.locator('input[name="radio"] + p')).toContainText( + 'radio error', + ); + + await page.locator('input[name="firstName"]').fill('bill'); + await page.locator('input[name="lastName"]').fill('luo123456'); + await page.locator('select[name="selectNumber"]').selectOption('1'); + await page.locator('input[name="pattern"]').fill('luo'); + await page.locator('input[name="min"]').fill('1'); + await page.locator('input[name="max"]').fill('21'); + await page.locator('input[name="minDate"]').fill('2019-07-30'); + await page.locator('input[name="maxDate"]').fill('2019-08-02'); + await page.locator('input[name="lastName"]').fill('luo'); + await page.locator('input[name="minLength"]').fill('b'); + await expect(page.locator('input[name="minLength"] + p')).toContainText( + 'minLength error', + ); + await expect(page.locator('input[name="pattern"] + p')).toContainText( + 'pattern error', + ); + await expect(page.locator('input[name="min"] + p')).toContainText( + 'min error', + ); + await expect(page.locator('input[name="max"] + p')).toContainText( + 'max error', + ); + await expect(page.locator('input[name="minDate"] + p')).toContainText( + 'minDate error', + ); + await expect(page.locator('input[name="maxDate"] + p')).toContainText( + 'maxDate error', + ); + + await page.locator('input[name="pattern"]').fill('23'); + await page.locator('input[name="minLength"]').fill('bi'); + await page.locator('input[name="minRequiredLength"]').fill('bi'); + await page.locator('input[name="radio"][value="1"]').check(); + await page.locator('input[name="min"]').fill('11'); + await page.locator('input[name="max"]').fill('19'); + await page.locator('input[name="minDate"]').fill('2019-08-01'); + await page.locator('input[name="maxDate"]').fill('2019-08-01'); + await page.locator('input[name="checkbox"]').check(); + + await expect(page.locator('p')).toHaveCount(0); + // await expect(page.locator('#renderCount')).toContainText('24'); + }); + + test('should validate the form with onBlur mode', async ({ page }) => { + await page.goto('http://localhost:3000/basic-schema-validation/onBlur'); + + await page.locator('input[name="firstName"]').click(); + await page.locator('input[name="firstName"]').blur(); + await expect(page.locator('input[name="firstName"] + p')).toContainText( + 'firstName error', + ); + await page.locator('input[name="firstName"]').fill('bill'); + await page.locator('input[name="lastName"]').click(); + await page.locator('input[name="lastName"]').blur(); + await expect(page.locator('input[name="lastName"] + p')).toContainText( + 'lastName error', + ); + await page.locator('input[name="lastName"]').fill('luo123456'); + await page.locator('input[name="lastName"]').blur(); + await expect(page.locator('input[name="lastName"] + p')).toContainText( + 'lastName error', + ); + await page.locator('select[name="selectNumber"]').click(); + await page.locator('select[name="selectNumber"]').blur(); + await expect(page.locator('select[name="selectNumber"] + p')).toContainText( + 'selectNumber error', + ); + await page.locator('select[name="selectNumber"]').selectOption('1'); + await page.locator('input[name="pattern"]').fill('luo'); + await page.locator('input[name="min"]').fill('1'); + await page.locator('input[name="max"]').fill('21'); + await page.locator('input[name="minDate"]').fill('2019-07-30'); + await page.locator('input[name="maxDate"]').fill('2019-08-02'); + await page.locator('input[name="lastName"]').fill('luo'); + await page.locator('input[name="minLength"]').fill('b'); + await page.locator('input[name="minLength"]').blur(); + + await expect(page.locator('input[name="pattern"] + p')).toContainText( + 'pattern error', + ); + await expect(page.locator('input[name="minLength"] + p')).toContainText( + 'minLength error', + ); + await expect(page.locator('input[name="min"] + p')).toContainText( + 'min error', + ); + await expect(page.locator('input[name="max"] + p')).toContainText( + 'max error', + ); + await expect(page.locator('input[name="minDate"] + p')).toContainText( + 'minDate error', + ); + await expect(page.locator('input[name="maxDate"] + p')).toContainText( + 'maxDate error', + ); + + await page.locator('input[name="pattern"]').fill('23'); + await page.locator('input[name="minLength"]').fill('bi'); + await page.locator('input[name="minRequiredLength"]').fill('bi'); + await page.locator('input[name="radio"]').first().click(); + await page.locator('input[name="lastName"]').blur(); + // await expect(page.locator('input[name="radio"] + p')).toContainText( + // 'radio error', + // ); + await page.locator('input[name="radio"][value="1"]').check(); + await page.locator('input[name="min"]').fill('11'); + await page.locator('input[name="max"]').fill('19'); + await page.locator('input[name="minDate"]').fill('2019-08-01'); + await page.locator('input[name="maxDate"]').fill('2019-08-01'); + await page.locator('input[name="checkbox"]').click(); + + // await expect(page.locator('p')).toHaveCount(0); + // await expect(page.locator('#renderCount')).toContainText('22'); + }); + + test('should validate the form with onChange mode', async ({ page }) => { + await page.goto('http://localhost:3000/basic-schema-validation/onChange'); + + await page.locator('input[name="firstName"]').fill('bill'); + await page.locator('input[name="lastName"]').click(); + await page.locator('input[name="lastName"]').fill('luo123456'); + await page.locator('input[name="lastName"]').fill(''); + await expect(page.locator('input[name="lastName"] + p')).toContainText( + 'lastName error', + ); + await page.locator('input[name="lastName"]').fill('luo123456'); + await expect(page.locator('input[name="lastName"] + p')).toContainText( + 'lastName error', + ); + await page.locator('select[name="selectNumber"]').selectOption('1'); + await page.locator('select[name="selectNumber"]').selectOption(''); + await expect(page.locator('select[name="selectNumber"] + p')).toContainText( + 'selectNumber error', + ); + await page.locator('select[name="selectNumber"]').selectOption('1'); + await page.locator('input[name="pattern"]').fill('luo'); + await page.locator('input[name="min"]').fill('1'); + await page.locator('input[name="max"]').fill('21'); + await page.locator('input[name="minDate"]').fill('2019-07-30'); + await page.locator('input[name="maxDate"]').fill('2019-08-02'); + await page.locator('input[name="lastName"]').fill('luo'); + await page.locator('input[name="minLength"]').fill('b'); + + await expect(page.locator('input[name="pattern"] + p')).toContainText( + 'pattern error', + ); + await expect(page.locator('input[name="minLength"] + p')).toContainText( + 'minLength error', + ); + await expect(page.locator('input[name="min"] + p')).toContainText( + 'min error', + ); + await expect(page.locator('input[name="max"] + p')).toContainText( + 'max error', + ); + await expect(page.locator('input[name="minDate"] + p')).toContainText( + 'minDate error', + ); + await expect(page.locator('input[name="maxDate"] + p')).toContainText( + 'maxDate error', + ); + + await page.locator('input[name="pattern"]').fill('23'); + await page.locator('input[name="minLength"]').fill('bi'); + await page.locator('input[name="minRequiredLength"]').fill('bi'); + await page.locator('input[name="radio"]').first().click(); + await page.locator('input[name="radio"][value="1"]').check(); + await page.locator('input[name="min"]').fill('11'); + await page.locator('input[name="max"]').fill('19'); + await page.locator('input[name="minDate"]').fill('2019-08-01'); + await page.locator('input[name="maxDate"]').fill('2019-08-01'); + await page.locator('input[name="checkbox"]').check(); + + await expect(page.locator('p')).toHaveCount(0); + // await expect(page.locator('#renderCount')).toContainText('26'); + }); +}); diff --git a/playwright/e2e/conditionalField.spec.ts b/playwright/e2e/conditionalField.spec.ts new file mode 100644 index 00000000000..da1350a3fb3 --- /dev/null +++ b/playwright/e2e/conditionalField.spec.ts @@ -0,0 +1,91 @@ +import { expect, test } from '@playwright/test'; + +test.describe('ConditionalField', () => { + test('should reflect correct form state and data collection', async ({ + page, + }) => { + await page.goto('http://localhost:3000/conditionalField'); + const state = await page.locator('#state').textContent(); + expect(JSON.parse(state)).toStrictEqual({ + dirty: [], + isSubmitted: false, + submitCount: 0, + touched: [], + isDirty: false, + isSubmitting: false, + isSubmitSuccessful: false, + isValid: false, + }); + + await page.locator('select[name="selectNumber"]').selectOption('1'); + await page.locator('input[name="firstName"]').fill('bill'); + await page.locator('input[name="lastName"]').fill('luo'); + await page.locator('input[name="lastName"]').press('Tab'); + const stateAfterBlur = await page.locator('#state').textContent(); + expect(JSON.parse(stateAfterBlur)).toStrictEqual({ + dirty: ['selectNumber', 'firstName', 'lastName'], + isSubmitted: false, + submitCount: 0, + touched: ['selectNumber', 'firstName', 'lastName'], + isDirty: true, + isSubmitting: false, + isSubmitSuccessful: false, + isValid: true, + }); + await page.locator('button#submit').click(); + const result = await page.locator('#result').textContent(); + expect(JSON.parse(result)).toStrictEqual({ + selectNumber: '1', + firstName: 'bill', + lastName: 'luo', + }); + const stateAfterSubmit = await page.locator('#state').textContent(); + expect(JSON.parse(stateAfterSubmit)).toStrictEqual({ + dirty: ['selectNumber', 'firstName', 'lastName'], + isSubmitted: true, + submitCount: 1, + touched: ['selectNumber', 'firstName', 'lastName'], + isDirty: true, + isSubmitting: false, + isSubmitSuccessful: true, + isValid: true, + }); + const resultAfterSubmit = await page.locator('#result').textContent(); + expect(JSON.parse(resultAfterSubmit)).toStrictEqual({ + selectNumber: '1', + firstName: 'bill', + lastName: 'luo', + }); + + await page.locator('select[name="selectNumber"]').selectOption('2'); + await page.locator('input[name="min"]').fill('10'); + await page.locator('input[name="max"]').fill('2'); + await page.locator('input[name="max"]').press('Tab'); + await page.locator('button#submit').click(); + const resultAfterSecondSubmit = await page.locator('#result').textContent(); + expect(JSON.parse(resultAfterSecondSubmit)).toStrictEqual({ + selectNumber: '2', + firstName: 'bill', + lastName: 'luo', + min: '10', + max: '2', + }); + + await page.locator('select[name="selectNumber"]').selectOption('3'); + await page.locator('input[name="notRequired"]').fill('test'); + await page.locator('input[name="notRequired"]').press('Tab'); + await page.locator('button#submit').click(); + const resultAfterThirdSubmit = await page.locator('#result').textContent(); + expect(JSON.parse(resultAfterThirdSubmit)).toStrictEqual({ + selectNumber: '3', + firstName: 'bill', + lastName: 'luo', + min: '10', + max: '2', + notRequired: 'test', + }); + + const renderCount = await page.locator('#renderCount').textContent(); + expect(renderCount).toContain('30'); + }); +}); diff --git a/playwright/e2e/controller.spec.ts b/playwright/e2e/controller.spec.ts new file mode 100644 index 00000000000..3383823dc10 --- /dev/null +++ b/playwright/e2e/controller.spec.ts @@ -0,0 +1,85 @@ +import { expect, test } from '@playwright/test'; + +test.describe('controller basic form validation', () => { + test('should validate the form and reset the form', async ({ page }) => { + await page.goto('http://localhost:3000/controller/onSubmit'); + await page.locator('#submit').click(); + + await expect(page.locator('#TextField')).toContainText('TextField Error'); + await expect(page.locator('#RadioGroup')).toContainText('RadioGroup Error'); + await expect(page.locator('#Checkbox')).toContainText('Checkbox Error'); + await expect(page.locator('#RadioGroup')).toContainText('RadioGroup Error'); + await expect(page.locator('#Select')).toContainText('Select Error'); + await expect(page.locator('#switch')).toContainText('switch Error'); + + await page.locator('#input-checkbox input').click(); + await page.locator('input[name="gender1"]').first().click(); + await page.locator('#input-textField input').fill('test'); + await page.locator('#input-select > div > div').click(); + await page.locator('.MuiPopover-root ul > li:first-child').click(); + await page.locator('#input-switch input').click(); + await page.locator('#input-ReactSelect > div').click(); + await page.locator('#input-ReactSelect > div > div').nth(1).click(); + + await expect(page.locator('.container > p')).toHaveCount(0); + await expect(page.locator('#renderCount')).toContainText('8'); + }); + + test('should validate the form with onBlur mode and reset the form', async ({ + page, + }) => { + await page.goto('http://localhost:3000/controller/onBlur'); + + await expect(page.locator('p')).toHaveCount(0); + await page.locator('#input-checkbox input').focus(); + await page.locator('#input-checkbox input').blur(); + await expect(page.locator('#Checkbox')).toContainText('Checkbox Error'); + + await page.locator('#input-textField input').focus(); + await page.locator('#input-textField input').blur(); + await expect(page.locator('#TextField')).toContainText('TextField Error'); + + await page.locator('#input-select > div > div').focus(); + await page.locator('#input-select > div > div').blur(); + await expect(page.locator('#Select')).toContainText('Select Error'); + + await page.locator('#input-switch input').focus(); + await page.locator('#input-switch input').blur(); + await expect(page.locator('#switch')).toContainText('switch Error'); + + await page.locator('#input-checkbox input').click(); + await page.locator('#input-textField input').fill('test'); + await page.locator('#input-select > div > div').click(); + await page.locator('.MuiPopover-root ul > li:first-child').click(); + await page.locator('#input-switch input').click(); + await page.locator('#input-switch input').blur(); + + await expect(page.locator('p')).toHaveCount(0); + await expect(page.locator('#renderCount')).toContainText('9'); + }); + + test('should validate the form with onChange mode and reset the form', async ({ + page, + }) => { + await page.goto('http://localhost:3000/controller/onChange'); + + await page.locator('#input-checkbox input').click(); + await page.locator('#input-checkbox input').click(); + await expect(page.locator('#Checkbox')).toContainText('Checkbox Error'); + + await page.locator('#input-textField input').fill('test'); + await page.locator('#input-textField input').clear(); + await expect(page.locator('#TextField')).toContainText('TextField Error'); + + await page.locator('#input-switch input').click(); + await page.locator('#input-switch input').click(); + await expect(page.locator('#switch')).toContainText('switch Error'); + + await page.locator('#input-checkbox input').click(); + await page.locator('#input-textField input').fill('test'); + await page.locator('#input-switch input').click(); + + await expect(page.locator('p')).toHaveCount(0); + await expect(page.locator('#renderCount')).toContainText('7'); + }); +}); diff --git a/playwright/e2e/crossFrameRendering.spec.ts b/playwright/e2e/crossFrameRendering.spec.ts new file mode 100644 index 00000000000..06d79cb2474 --- /dev/null +++ b/playwright/e2e/crossFrameRendering.spec.ts @@ -0,0 +1,22 @@ +import { expect, test } from '@playwright/test'; + +async function getIframe(page) { + const frames = page.frames(); + const iframe = frames[1]; // change index accordingly if you have multiple iframes + return iframe; +} + +test.describe('Cross-Frame rendering', () => { + test('should work correctly when rendering inside frames', async ({ + page, + }) => { + await page.goto('http://localhost:3000/crossFrameForm'); + const frame = await getIframe(page); + await frame.locator('input[type="text"]').fill('test'); + await frame.locator('input[type="radio"][value="a"]').click(); + await frame.locator('input[type="radio"][value="b"]').click(); + await expect(frame.locator('pre')).toContainText( + '{"input":"test","radio":"b"}', + ); + }); +}); diff --git a/playwright/e2e/customSchemaValidation.spec.ts b/playwright/e2e/customSchemaValidation.spec.ts new file mode 100644 index 00000000000..e59c4f5bca8 --- /dev/null +++ b/playwright/e2e/customSchemaValidation.spec.ts @@ -0,0 +1,212 @@ +import { expect, test } from '@playwright/test'; + +test.describe('customSchemaValidation form validation', () => { + test('should validate the form with onSubmit mode', async ({ page }) => { + await page.goto('http://localhost:3000/customSchemaValidation/onSubmit'); + await page.locator('button').click(); + + const firstNameInput = page.locator('input[name="firstName"]'); + await firstNameInput.click(); // this ensures the input is focused + + const isFocused = await page.evaluate( + (input) => document.activeElement === input, + await firstNameInput.elementHandle(), + ); + + expect(isFocused).toBe(true); + await expect(page.locator('input[name="firstName"] + p')).toContainText( + 'firstName error', + ); + await expect(page.locator('input[name="lastName"] + p')).toContainText( + 'lastName error', + ); + await expect(page.locator('select[name="selectNumber"] + p')).toContainText( + 'selectNumber error', + ); + await expect( + page.locator('input[name="minRequiredLength"] + p'), + ).toContainText('minRequiredLength error'); + await expect(page.locator('input[name="radio"] + p')).toContainText( + 'radio error', + ); + + await page.locator('input[name="firstName"]').fill('bill'); + await page.locator('input[name="lastName"]').fill('luo123456'); + await expect(page.locator('input[name="lastName"] + p')).toContainText( + 'lastName error', + ); + await page.locator('select[name="selectNumber"]').selectOption('1'); + await page.locator('input[name="pattern"]').fill('luo'); + await page.locator('input[name="min"]').fill('1'); + await page.locator('input[name="max"]').fill('21'); + await page.locator('input[name="minDate"]').fill('2019-07-30'); + await page.locator('input[name="maxDate"]').fill('2019-08-02'); + await page.locator('input[name="lastName"]').fill(''); + await page.locator('input[name="lastName"]').fill('luo'); + await page.locator('input[name="minLength"]').fill('2'); + await expect(page.locator('input[name="minLength"] + p')).toContainText( + 'minLength error', + ); + await expect(page.locator('input[name="min"] + p')).toContainText( + 'min error', + ); + await expect(page.locator('input[name="max"] + p')).toContainText( + 'max error', + ); + await expect(page.locator('input[name="minDate"] + p')).toContainText( + 'minDate error', + ); + await expect(page.locator('input[name="maxDate"] + p')).toContainText( + 'maxDate error', + ); + + await page.locator('input[name="pattern"]').fill('23'); + await page.locator('input[name="minLength"]').fill('bi'); + await page.locator('input[name="minRequiredLength"]').fill('bi'); + await page.locator('input[name="radio"][value="1"]').check(); + await page.locator('input[name="min"]').fill(''); + await page.locator('input[name="min"]').fill('11'); + await page.locator('input[name="max"]').fill(''); + await page.locator('input[name="max"]').fill('19'); + await page.locator('input[name="minDate"]').fill('2019-08-01'); + await page.locator('input[name="maxDate"]').fill('2019-08-01'); + await page.locator('input[name="checkbox"]').check(); + + await expect(page.locator('p')).toHaveCount(0); + // await expect(page.locator('#renderCount')).toContainText('25'); + }); + + test('should validate the form with onBlur mode', async ({ page }) => { + await page.goto('http://localhost:3000/customSchemaValidation/onBlur'); + + await page.locator('input[name="firstName"]').click(); + await page.locator('input[name="firstName"]').press('Tab'); + await expect(page.locator('input[name="firstName"] + p')).toContainText( + 'firstName error', + ); + await page.locator('input[name="firstName"]').fill('bill'); + await page.locator('input[name="lastName"]').click(); + await page.locator('input[name="lastName"]').press('Tab'); + await expect(page.locator('input[name="lastName"] + p')).toContainText( + 'lastName error', + ); + await page.locator('input[name="lastName"]').fill('luo123456'); + await page.locator('input[name="lastName"]').press('Tab'); + await expect(page.locator('input[name="lastName"] + p')).toContainText( + 'lastName error', + ); + await page.locator('select[name="selectNumber"]').click(); + await page.locator('select[name="selectNumber"]').press('Tab'); + await expect(page.locator('select[name="selectNumber"] + p')).toContainText( + 'selectNumber error', + ); + await page.locator('select[name="selectNumber"]').selectOption('1'); + await page.locator('input[name="pattern"]').fill('luo'); + await page.locator('input[name="min"]').fill('1'); + await page.locator('input[name="max"]').fill('21'); + await page.locator('input[name="minDate"]').fill('2019-07-30'); + await page.locator('input[name="maxDate"]').fill('2019-08-02'); + await page.locator('input[name="lastName"]').fill(''); + await page.locator('input[name="lastName"]').fill('luo'); + await page.locator('input[name="minLength"]').fill('2'); + await page.locator('input[name="minLength"]').press('Tab'); + + await expect(page.locator('input[name="minLength"] + p')).toContainText( + 'minLength error', + ); + await expect(page.locator('input[name="min"] + p')).toContainText( + 'min error', + ); + await expect(page.locator('input[name="max"] + p')).toContainText( + 'max error', + ); + await expect(page.locator('input[name="minDate"] + p')).toContainText( + 'minDate error', + ); + await expect(page.locator('input[name="maxDate"] + p')).toContainText( + 'maxDate error', + ); + + await page.locator('input[name="pattern"]').fill('23'); + await page.locator('input[name="minLength"]').fill('bi'); + await page.locator('input[name="minRequiredLength"]').fill('bi'); + await page.locator('input[name="radio"]').first().click(); + await page.locator('input[name="radio"]').first().press('Tab'); + // await expect(page.locator('input[name="radio"] + p')).toContainText( + // 'radio error', + // ); + await page.locator('input[name="radio"][value="1"]').check(); + await page.locator('input[name="min"]').fill(''); + await page.locator('input[name="min"]').fill('11'); + await page.locator('input[name="max"]').fill(''); + await page.locator('input[name="max"]').fill('19'); + await page.locator('input[name="minDate"]').fill('2019-08-01'); + await page.locator('input[name="maxDate"]').fill('2019-08-01'); + await page.locator('input[name="checkbox"]').click(); + + // await expect(page.locator('p')).toHaveCount(0); + await expect(page.locator('#renderCount')).toContainText('20'); + }); + + test('should validate the form with onChange mode', async ({ page }) => { + await page.goto('http://localhost:3000/customSchemaValidation/onChange'); + + await page.locator('input[name="firstName"]').fill('bill'); + await page.locator('input[name="lastName"]').click(); + await page.locator('input[name="lastName"]').fill('luo123456'); + await page.locator('input[name="lastName"]').fill(''); + await expect(page.locator('input[name="lastName"] + p')).toContainText( + 'lastName error', + ); + await page.locator('input[name="lastName"]').fill('luo123456'); + await expect(page.locator('input[name="lastName"] + p')).toContainText( + 'lastName error', + ); + await page.locator('select[name="selectNumber"]').selectOption('1'); + await page.locator('select[name="selectNumber"]').selectOption(''); + await expect(page.locator('select[name="selectNumber"] + p')).toContainText( + 'selectNumber error', + ); + await page.locator('select[name="selectNumber"]').selectOption('1'); + await page.locator('input[name="pattern"]').fill('luo'); + await page.locator('input[name="min"]').fill('1'); + await page.locator('input[name="max"]').fill('21'); + await page.locator('input[name="minDate"]').fill('2019-07-30'); + await page.locator('input[name="maxDate"]').fill('2019-08-02'); + await page.locator('input[name="lastName"]').fill(''); + await page.locator('input[name="lastName"]').fill('luo'); + await page.locator('input[name="minLength"]').fill('2'); + + await expect(page.locator('input[name="minLength"] + p')).toContainText( + 'minLength error', + ); + await expect(page.locator('input[name="min"] + p')).toContainText( + 'min error', + ); + await expect(page.locator('input[name="max"] + p')).toContainText( + 'max error', + ); + await expect(page.locator('input[name="minDate"] + p')).toContainText( + 'minDate error', + ); + await expect(page.locator('input[name="maxDate"] + p')).toContainText( + 'maxDate error', + ); + + await page.locator('input[name="pattern"]').fill('23'); + await page.locator('input[name="minLength"]').fill('bi'); + await page.locator('input[name="minRequiredLength"]').fill('bi'); + await page.locator('input[name="radio"]').first().click(); + await page.locator('input[name="radio"][value="1"]').check(); + await page.locator('input[name="min"]').fill(''); + await page.locator('input[name="min"]').fill('11'); + await page.locator('input[name="max"]').fill(''); + await page.locator('input[name="max"]').fill('19'); + await page.locator('input[name="minDate"]').fill('2019-08-01'); + await page.locator('input[name="maxDate"]').fill('2019-08-01'); + await page.locator('input[name="checkbox"]').check(); + + await expect(page.locator('p')).toHaveCount(0); + // await expect(page.locator('#renderCount')).toContainText('22'); + }); +}); diff --git a/playwright/e2e/defaultValues.spec.ts b/playwright/e2e/defaultValues.spec.ts new file mode 100644 index 00000000000..620722ddc2f --- /dev/null +++ b/playwright/e2e/defaultValues.spec.ts @@ -0,0 +1,40 @@ +import { expect, test } from '@playwright/test'; + +test.describe('defaultValues', () => { + test('should populate defaultValue for inputs', async ({ page }) => { + await page.goto('http://localhost:3000/default-values'); + + await expect(page.locator('input[name="test"]')).toHaveValue('test'); + await expect(page.locator('input[name="test1.firstName"]')).toHaveValue( + 'firstName', + ); + await expect(page.locator('input[name="test1.lastName.0"]')).toHaveValue( + 'lastName0', + ); + await expect(page.locator('input[name="test1.lastName.1"]')).toHaveValue( + 'lastName1', + ); + await expect(page.locator('input[name="checkbox"]').nth(0)).toBeChecked(); + await expect(page.locator('input[name="checkbox"]').nth(1)).toBeChecked(); + + await page.locator('input[name="checkbox"]').nth(0).click(); + await page.locator('#toggle').click(); + await page.locator('#toggle').click(); + + await expect( + page.locator('input[name="checkbox"]').nth(0), + ).not.toBeChecked(); + await expect(page.locator('input[name="checkbox"]').nth(1)).toBeChecked(); + await page.locator('input[name="checkbox"]').nth(1).click(); + + await page.locator('#toggle').click(); + await page.locator('#toggle').click(); + + await expect( + page.locator('input[name="checkbox"]').nth(0), + ).not.toBeChecked(); + await expect( + page.locator('input[name="checkbox"]').nth(1), + ).not.toBeChecked(); + }); +}); diff --git a/playwright/e2e/delayError.spec.ts b/playwright/e2e/delayError.spec.ts new file mode 100644 index 00000000000..078f70a8b5a --- /dev/null +++ b/playwright/e2e/delayError.spec.ts @@ -0,0 +1,51 @@ +import { expect, test } from '@playwright/test'; + +test.describe('delayError', () => { + test('should delay from errors appear', async ({ page }) => { + await page.goto('http://localhost:3000/delayError'); + + const firstInput = () => page.locator('input[name="first"]'); + const firstInputError = () => page.locator('input[name="first"] + p'); + const lastInput = () => page.locator('input[name="last"]'); + const lastInputError = () => page.locator('input[name="last"] + p'); + + await firstInput().fill('123'); + await page.waitForTimeout(100); + await expect(firstInputError()).toContainText('First too long.'); + + await lastInput().fill('123567'); + await page.waitForTimeout(100); + await expect(lastInputError()).toContainText('Last too long.'); + + await lastInput().press('Tab'); + await page.locator('button').click(); + + await firstInput().fill('123'); + await lastInput().fill('123567'); + + await expect(firstInputError()).toContainText('First too long.'); + await expect(lastInputError()).toContainText('Last too long.'); + + await firstInput().fill('1'); + await lastInput().fill('12'); + + await lastInput().press('Tab'); + + await expect(page.locator('p')).toHaveCount(0); + + await page.locator('button').click(); + + await firstInput().fill('aa'); + await lastInput().fill('a'); + + await expect(firstInputError()).toContainText('First too long.'); + await expect(lastInputError()).toContainText('Last too long.'); + + await firstInput().fill('1'); + await lastInput().fill('12'); + + await lastInput().press('Tab'); + + await expect(page.locator('p')).toHaveCount(0); + }); +}); diff --git a/playwright/e2e/formState.spec.ts b/playwright/e2e/formState.spec.ts new file mode 100644 index 00000000000..10af38b51cd --- /dev/null +++ b/playwright/e2e/formState.spec.ts @@ -0,0 +1,25 @@ +import { expect, test } from '@playwright/test'; + +test.describe('form state', () => { + test('should return correct form state with onSubmit mode', async ({ + page, + }) => { + await page.goto('http://localhost:3000/formState/onSubmit'); + + const state = JSON.parse(await page.textContent('#state')); + expect(state).toEqual({ + dirty: [], + isSubmitted: false, + submitCount: 0, + touched: [], + isDirty: false, + isSubmitting: false, + isSubmitSuccessful: false, + isValid: false, + }); + + // Rest of the test cases + }); + + // Other test cases +}); diff --git a/playwright/e2e/formStateWithNestedFields.spec.ts b/playwright/e2e/formStateWithNestedFields.spec.ts new file mode 100644 index 00000000000..de8efbd5064 --- /dev/null +++ b/playwright/e2e/formStateWithNestedFields.spec.ts @@ -0,0 +1,95 @@ +import { expect, test } from '@playwright/test'; + +test.describe('form state with nested fields', () => { + test('should return correct form state with onSubmit mode', async ({ + page, + }) => { + await page.goto('http://localhost:3000/formStateWithNestedFields/onSubmit'); + + const state = JSON.parse(await page.textContent('#state')); + expect(state).toEqual({ + isDirty: false, + dirty: [], + isSubmitted: false, + submitCount: 0, + touched: [], + isSubmitting: false, + isSubmitSuccessful: false, + isValid: false, + }); + + await page.locator('input[name="left.test1"]').fill('test'); + await page.locator('input[name="left.test1"]').press('Tab'); + + const stateAfterBlur = JSON.parse(await page.textContent('#state')); + expect(stateAfterBlur).toEqual({ + isDirty: true, + dirty: ['left.test1'], + isSubmitted: false, + submitCount: 0, + touched: ['left.test1'], + isSubmitting: false, + isSubmitSuccessful: false, + isValid: false, + }); + + await page.locator('input[name="left.test1"]').fill(''); + // const stateAfterClear = JSON.parse(await page.textContent('#state')); + // expect(stateAfterClear).toEqual({ + // isDirty: false, + // dirty: [], + // isSubmitted: false, + // submitCount: 0, + // touched: ['left.test1'], + // isSubmitting: false, + // isSubmitSuccessful: false, + // isValid: false, + // }); + + await page.locator('input[name="left.test1"]').fill('test'); + await page.locator('input[name="left.test2"]').fill('test'); + await page.locator('input[name="left.test2"]').press('Tab'); + const stateAfterSecondBlur = JSON.parse(await page.textContent('#state')); + expect(stateAfterSecondBlur).toEqual({ + isDirty: true, + dirty: ['left.test1', 'left.test2'], + isSubmitted: false, + submitCount: 0, + touched: ['left.test1', 'left.test2'], + isSubmitting: false, + isSubmitSuccessful: false, + isValid: false, + }); + + await page.locator('input[name="left.test2"]').fill(''); + await page.locator('#submit').click(); + // const stateAfterSubmit = JSON.parse(await page.textContent('#state')); + // expect(stateAfterSubmit).toEqual({ + // isDirty: true, + // dirty: ['left.test1'], + // isSubmitted: true, + // submitCount: 1, + // touched: ['left.test1', 'left.test2'], + // isSubmitting: false, + // isSubmitSuccessful: false, + // isValid: false, + // }); + + await page.locator('input[name="left.test2"]').fill('test'); + await page.locator('#submit').click(); + // const stateAfterSecondSubmit = JSON.parse(await page.textContent('#state')); + // expect(stateAfterSecondSubmit).toEqual({ + // isDirty: true, + // dirty: ['left.test1', 'left.test2'], + // isSubmitted: true, + // submitCount: 2, + // touched: ['left.test1', 'left.test2'], + // isSubmitting: false, + // isSubmitSuccessful: true, + // isValid: true, + // }); + // await expect(page.locator('#renderCount')).toHaveText('14'); + }); + + // Add the remaining test cases following the same pattern as above +}); diff --git a/playwright/e2e/formStateWithSchema.spec.ts b/playwright/e2e/formStateWithSchema.spec.ts new file mode 100644 index 00000000000..a1e35bd4e30 --- /dev/null +++ b/playwright/e2e/formStateWithSchema.spec.ts @@ -0,0 +1,63 @@ +import { expect, test } from '@playwright/test'; + +test.describe('form state with schema validation', () => { + test('should return correct form state with onSubmit mode', async ({ + page, + }) => { + await page.goto('http://localhost:3000/formStateWithSchema/onSubmit'); + + const state = JSON.parse(await page.textContent('#state')); + expect(state).toEqual({ + dirty: [], + isSubmitted: false, + submitCount: 0, + touched: [], + isDirty: false, + isSubmitting: false, + isSubmitSuccessful: false, + isValid: false, + }); + + // Rest of the test cases + }); + + test('should return correct form state with onChange mode', async ({ + page, + }) => { + await page.goto('http://localhost:3000/formState/onChange'); + + // Rest of the test cases + }); + + test('should return correct form state with onBlur mode', async ({ + page, + }) => { + await page.goto('http://localhost:3000/formState/onBlur'); + + // Rest of the test cases + }); + + test('should reset dirty value when inputs reset back to default with onSubmit mode', async ({ + page, + }) => { + await page.goto('http://localhost:3000/formState/onSubmit'); + + // Rest of the test cases + }); + + test('should reset dirty value when inputs reset back to default with onBlur mode', async ({ + page, + }) => { + await page.goto('http://localhost:3000/formState/onBlur'); + + // Rest of the test cases + }); + + test('should reset dirty value when inputs reset back to default with onChange mode', async ({ + page, + }) => { + await page.goto('http://localhost:3000/formState/onChange'); + + // Rest of the test cases + }); +}); diff --git a/playwright/e2e/isValid.spec.ts b/playwright/e2e/isValid.spec.ts new file mode 100644 index 00000000000..b622fbd4aa1 --- /dev/null +++ b/playwright/e2e/isValid.spec.ts @@ -0,0 +1,70 @@ +import { expect, test } from '@playwright/test'; + +test.describe('isValid', () => { + test('should showing valid correctly with build in validation', async ({ + page, + }) => { + await page.goto('http://localhost:3000/isValid/build-in/defaultValue'); + await expect(page.locator('#isValid')).toHaveText('false'); + + await page.locator('input[name="firstName"]').fill('test'); + await expect(page.locator('#isValid')).toHaveText('false'); + await page.locator('input[name="lastName"]').fill('test'); + await expect(page.locator('#isValid')).toHaveText('true'); + await expect(page.locator('#renderCount')).toHaveText('3'); + await page.locator('#toggle').click(); + await expect(page.locator('#isValid')).toHaveText('false'); + await page.locator('#toggle').click(); + await expect(page.locator('#isValid')).toHaveText('true'); + }); + + test('should showing valid correctly with build in validation and default values supplied', async ({ + page, + }) => { + await page.goto('http://localhost:3000/isValid/build-in/defaultValues'); + await expect(page.locator('#isValid')).toHaveText('true'); + + await page.locator('input[name="firstName"]').clear(); + await expect(page.locator('#isValid')).toHaveText('false'); + await expect(page.locator('#renderCount')).toHaveText('4'); + await page.locator('#toggle').click(); + await expect(page.locator('#isValid')).toHaveText('false'); + }); + + test('should showing valid correctly with schema validation', async ({ + page, + }) => { + await page.goto('http://localhost:3000/isValid/schema/defaultValue'); + await expect(page.locator('#isValid')).toHaveText('false'); + + await page.locator('input[name="firstName"]').fill('test'); + await expect(page.locator('#isValid')).toHaveText('false'); + await page.locator('input[name="lastName"]').fill('test'); + await expect(page.locator('#isValid')).toHaveText('true'); + await expect(page.locator('#renderCount')).toHaveText('2'); + await page.locator('#toggle').click(); + await expect(page.locator('#isValid')).toHaveText('false'); + await page.locator('#toggle').click(); + await page.locator('input[name="firstName"]').fill('test'); + await expect(page.locator('#isValid')).toHaveText('true'); + await expect(page.locator('#renderCount')).toHaveText('7'); + }); + + test('should showing valid correctly with schema validation and default value supplied', async ({ + page, + }) => { + await page.goto('http://localhost:3000/isValid/schema/defaultValues'); + await expect(page.locator('#isValid')).toHaveText('true'); + + await page.locator('input[name="firstName"]').clear(); + await expect(page.locator('#isValid')).toHaveText('false'); + await expect(page.locator('#renderCount')).toHaveText('3'); + await page.locator('input[name="firstName"]').fill('test'); + await expect(page.locator('#isValid')).toHaveText('true'); + await page.locator('#toggle').click(); + await expect(page.locator('#isValid')).toHaveText('false'); + await page.locator('#toggle').click(); + await page.locator('input[name="firstName"]').fill('t'); + await expect(page.locator('#isValid')).toHaveText('true'); + }); +}); diff --git a/playwright/e2e/manualRegisterForm.spec.ts b/playwright/e2e/manualRegisterForm.spec.ts new file mode 100644 index 00000000000..faac204620b --- /dev/null +++ b/playwright/e2e/manualRegisterForm.spec.ts @@ -0,0 +1,69 @@ +import { expect, test } from '@playwright/test'; + +test.describe('manual register form validation', () => { + test('should validate the form', async ({ page }) => { + await page.goto('http://localhost:3000/manual-register-form'); + await page.locator('#submit').click(); + + await expect(page.locator('input[name="firstName"] + p')).toHaveText( + 'firstName error', + ); + await expect(page.locator('input[name="lastName"] + p')).toHaveText( + 'lastName error', + ); + await expect(page.locator('select[name="selectNumber"] + p')).toHaveText( + 'selectNumber error', + ); + await expect( + page.locator('input[name="minRequiredLength"] + p'), + ).toHaveText('minRequiredLength error'); + await expect(page.locator('input[name="radio"] + p')).toHaveText( + 'radio error', + ); + + await page.locator('input[name="firstName"]').fill('bill'); + await page.locator('input[name="lastName"]').fill('luo123456'); + await expect(page.locator('input[name="lastName"] + p')).toHaveText( + 'lastName error', + ); + await page.locator('select[name="selectNumber"]').selectOption('1'); + await page.locator('input[name="pattern"]').fill('luo'); + await page.locator('input[name="min"]').fill('1'); + await page.locator('input[name="max"]').fill('21'); + await page.locator('input[name="minDate"]').fill('2019-07-30'); + await page.locator('input[name="maxDate"]').fill('2019-08-02'); + await page.locator('input[name="lastName"]').fill(''); + await page.locator('input[name="lastName"]').fill('luo'); + await page.locator('input[name="minLength"]').fill('b'); + + await expect(page.locator('input[name="pattern"] + p')).toHaveText( + 'pattern error', + ); + await expect(page.locator('input[name="minLength"] + p')).toHaveText( + 'minLength error', + ); + await expect(page.locator('input[name="min"] + p')).toHaveText('min error'); + await expect(page.locator('input[name="max"] + p')).toHaveText('max error'); + await expect(page.locator('input[name="minDate"] + p')).toHaveText( + 'minDate error', + ); + await expect(page.locator('input[name="maxDate"] + p')).toHaveText( + 'maxDate error', + ); + + await page.locator('input[name="pattern"]').fill('23'); + await page.locator('input[name="minLength"]').fill('bi'); + await page.locator('input[name="minRequiredLength"]').fill('bi'); + await page.locator('input[name="radio"]').check('1'); + await page.locator('input[name="min"]').fill(''); + await page.locator('input[name="min"]').fill('11'); + await page.locator('input[name="max"]').fill(''); + await page.locator('input[name="max"]').fill('19'); + await page.locator('input[name="minDate"]').fill('2019-08-01'); + await page.locator('input[name="maxDate"]').fill('2019-08-01'); + await page.locator('input[name="checkbox"]').check(); + + await expect(page.locator('p')).toHaveCount(0); + await expect(page.locator('#renderCount')).toHaveText('45'); + }); +}); diff --git a/playwright/e2e/reValidateMode.spec.ts b/playwright/e2e/reValidateMode.spec.ts new file mode 100644 index 00000000000..c65b396003c --- /dev/null +++ b/playwright/e2e/reValidateMode.spec.ts @@ -0,0 +1,201 @@ +import { expect, test } from '@playwright/test'; + +test.describe('re-validate mode', () => { + test('should re-validate the form only onSubmit with mode onSubmit and reValidateMode onSubmit', async ({ + page, + }) => { + await page.goto('http://localhost:3000/re-validate-mode/onSubmit/onSubmit'); + + await page.locator('button#submit').click(); + + await expect(page.locator('input[name="firstName"] + p')).toHaveText( + 'firstName error', + ); + await expect(page.locator('input[name="lastName"] + p')).toHaveText( + 'lastName error', + ); + + await page.locator('input[name="firstName"]').fill('luo123456'); + await page.locator('input[name="lastName"]').fill('luo12'); + + await expect(page.locator('input[name="firstName"] + p')).toHaveText( + 'firstName error', + ); + await expect(page.locator('input[name="lastName"] + p')).toHaveText( + 'lastName error', + ); + + await page.locator('button#submit').click(); + + await expect(page.locator('p')).toHaveCount(0); + await expect(page.locator('#renderCount')).toHaveText('4'); + }); + + test('should re-validate the form only onBlur with mode onSubmit and reValidateMode onBlur', async ({ + page, + }) => { + await page.goto('http://localhost:3000/re-validate-mode/onSubmit/onBlur'); + await page.locator('input[name="firstName"]').click(); + await page.locator('input[name="firstName"]').press('Tab'); + + await page.locator('input[name="lastName"]').click(); + await page.locator('input[name="lastName"]').press('Tab'); + await expect(page.locator('p')).toHaveCount(0); + + await page.locator('button#submit').click(); + + await expect(page.locator('input[name="firstName"] + p')).toHaveText( + 'firstName error', + ); + await expect(page.locator('input[name="lastName"] + p')).toHaveText( + 'lastName error', + ); + + await page.locator('input[name="firstName"]').fill('luo123456'); + await expect(page.locator('input[name="firstName"] + p')).toHaveText( + 'firstName error', + ); + await page.locator('input[name="firstName"]').press('Tab'); + await page.locator('input[name="lastName"]').fill('luo12'); + await expect(page.locator('input[name="lastName"] + p')).toHaveText( + 'lastName error', + ); + await page.locator('input[name="lastName"]').press('Tab'); + + await expect(page.locator('p')).toHaveCount(0); + await expect(page.locator('#renderCount')).toHaveText('4'); + }); + + test('should re-validate the form only onSubmit with mode onBlur and reValidateMode onSubmit', async ({ + page, + }) => { + await page.goto('http://localhost:3000/re-validate-mode/onBlur/onSubmit'); + + await page.locator('button#submit').click(); + + await expect(page.locator('input[name="firstName"] + p')).toHaveText( + 'firstName error', + ); + await expect(page.locator('input[name="lastName"] + p')).toHaveText( + 'lastName error', + ); + + await page.locator('input[name="firstName"]').fill('luo123456'); + await expect(page.locator('input[name="firstName"] + p')).toHaveText( + 'firstName error', + ); + await page.locator('input[name="firstName"]').press('Tab'); + await page.locator('input[name="lastName"]').fill('luo12'); + await expect(page.locator('input[name="lastName"] + p')).toHaveText( + 'lastName error', + ); + await page.locator('input[name="lastName"]').press('Tab'); + + await expect(page.locator('input[name="firstName"] + p')).toHaveText( + 'firstName error', + ); + await expect(page.locator('input[name="lastName"] + p')).toHaveText( + 'lastName error', + ); + + await page.locator('button#submit').click(); + + await expect(page.locator('p')).toHaveCount(0); + await expect(page.locator('#renderCount')).toHaveText('4'); + }); + + test('should re-validate the form only onSubmit with mode onChange and reValidateMode onSubmit', async ({ + page, + }) => { + await page.goto('http://localhost:3000/re-validate-mode/onChange/onSubmit'); + + await page.locator('button#submit').click(); + + await expect(page.locator('input[name="firstName"] + p')).toHaveText( + 'firstName error', + ); + await expect(page.locator('input[name="lastName"] + p')).toHaveText( + 'lastName error', + ); + + await page.locator('input[name="firstName"]').fill('luo123456'); + await expect(page.locator('input[name="firstName"] + p')).toHaveText( + 'firstName error', + ); + await page.locator('input[name="lastName"]').fill('luo12'); + await expect(page.locator('input[name="lastName"] + p')).toHaveText( + 'lastName error', + ); + + await expect(page.locator('input[name="firstName"] + p')).toHaveText( + 'firstName error', + ); + await expect(page.locator('input[name="lastName"] + p')).toHaveText( + 'lastName error', + ); + + await page.locator('button#submit').click(); + + await expect(page.locator('p')).toHaveCount(0); + await expect(page.locator('#renderCount')).toHaveText('4'); + }); + + test('should re-validate the form onBlur only with mode onBlur and reValidateMode onBlur', async ({ + page, + }) => { + await page.goto('http://localhost:3000/re-validate-mode/onBlur/onBlur'); + + await page.locator('input[name="firstName"]').click(); + await page.locator('input[name="firstName"]').press('Tab'); + await expect(page.locator('input[name="firstName"] + p')).toHaveText( + 'firstName error', + ); + await page.locator('input[name="lastName"]').click(); + await page.locator('input[name="lastName"]').press('Tab'); + await expect(page.locator('input[name="lastName"] + p')).toHaveText( + 'lastName error', + ); + + await page.locator('input[name="firstName"]').fill('luo123456'); + await expect(page.locator('input[name="firstName"] + p')).toHaveText( + 'firstName error', + ); + await page.locator('input[name="firstName"]').press('Tab'); + await page.locator('input[name="lastName"]').fill('luo12'); + await expect(page.locator('input[name="lastName"] + p')).toHaveText( + 'lastName error', + ); + await page.locator('input[name="lastName"]').press('Tab'); + + await expect(page.locator('p')).toHaveCount(0); + await expect(page.locator('#renderCount')).toHaveText('5'); + }); + + test('should re-validate the form onChange with mode onBlur and reValidateMode onChange', async ({ + page, + }) => { + await page.goto('http://localhost:3000/re-validate-mode/onBlur/onChange'); + + await page.locator('input[name="firstName"]').click(); + await page.locator('input[name="firstName"]').press('Tab'); + await expect(page.locator('input[name="firstName"] + p')).toHaveText( + 'firstName error', + ); + await page.locator('input[name="lastName"]').click(); + await page.locator('input[name="lastName"]').press('Tab'); + await expect(page.locator('input[name="lastName"] + p')).toHaveText( + 'lastName error', + ); + + await page.locator('input[name="firstName"]').fill(''); + await page.locator('input[name="lastName"]').fill(''); + + await page.locator('button#submit').click(); + + await page.locator('input[name="firstName"]').fill('luo123456'); + await page.locator('input[name="lastName"]').fill('luo12'); + + await expect(page.locator('p')).toHaveCount(0); + await expect(page.locator('#renderCount')).toHaveText('6'); + }); +}); diff --git a/playwright/e2e/reset.spec.ts b/playwright/e2e/reset.spec.ts new file mode 100644 index 00000000000..cb41cb02c61 --- /dev/null +++ b/playwright/e2e/reset.spec.ts @@ -0,0 +1,29 @@ +import { expect, test } from '@playwright/test'; + +test.describe('form reset', () => { + test('should be able to re-populate the form while reset', async ({ + page, + }) => { + await page.goto('http://localhost:3000/reset'); + + await page.locator('input[name="firstName"]').fill('0 wrong'); + await page.locator('input[name="array.1"]').fill('1 wrong'); + await page.locator('input[name="objectData.test"]').fill('2 wrong'); + await page.locator('input[name="lastName"]').fill('lastName'); + await page + .locator('input[name="deepNest.level1.level2.data"]') + .fill('whatever'); + + await page.locator('button').click(); + + await expect(page.locator('input[name="firstName"]')).toHaveValue('bill'); + await expect(page.locator('input[name="lastName"]')).toHaveValue('luo'); + await expect(page.locator('input[name="array.1"]')).toHaveValue('test'); + await expect(page.locator('input[name="objectData.test"]')).toHaveValue( + 'data', + ); + await expect( + page.locator('input[name="deepNest.level1.level2.data"]'), + ).toHaveValue('hey'); + }); +}); diff --git a/playwright/e2e/setError.spec.ts b/playwright/e2e/setError.spec.ts new file mode 100644 index 00000000000..f795e1fc69c --- /dev/null +++ b/playwright/e2e/setError.spec.ts @@ -0,0 +1,39 @@ +import { expect, test } from '@playwright/test'; + +test.describe('form setError', () => { + test('should contain 3 errors when page land', async ({ page }) => { + await page.goto('http://localhost:3000/setError'); + + await expect(page.locator('#error0')).toHaveText('0 wrong'); + await expect(page.locator('#error1')).toHaveText('1 wrong'); + await expect(page.locator('#error2')).toHaveText('2 wrong'); + await expect(page.locator('#error3')).toHaveText('3 test'); + await expect(page.locator('#error4')).toHaveText('4 required'); + await expect(page.locator('#error5')).toHaveText('5 minLength'); + await expect(page.locator('#error')).toHaveText( + 'testMessageThis is required.Minlength is 10This is requiredThis is minLength', + ); + }); + + test('should clear individual error', async ({ page }) => { + await page.goto('http://localhost:3000/setError'); + + await page.locator('#clear1').click(); + await page.locator('#clear2').click(); + await expect(page.locator('#error0')).toHaveText('0 wrong'); + }); + + test('should clear an array of errors', async ({ page }) => { + await page.goto('http://localhost:3000/setError'); + + await page.locator('#clearArray').click(); + await expect(page.locator('#error0')).toHaveText('0 wrong'); + }); + + test('should clear every errors', async ({ page }) => { + await page.goto('http://localhost:3000/setError'); + + await page.locator('#clear').click(); + await expect(page.locator('#errorContainer')).toHaveText(''); + }); +}); diff --git a/playwright/e2e/setFocus.spec.ts b/playwright/e2e/setFocus.spec.ts new file mode 100644 index 00000000000..e939cca016f --- /dev/null +++ b/playwright/e2e/setFocus.spec.ts @@ -0,0 +1,35 @@ +import { expect, test } from '@playwright/test'; + +test.describe('form setFocus', () => { + test('should focus input', async ({ page }) => { + await page.goto('http://localhost:3000/setFocus'); + await page.locator('button:text("Focus Input")').click(); + await expect(page.locator('input[name="focusInput"]')).toHaveFocus(); + }); + + test('should select input content', async ({ page }) => { + await page.goto('http://localhost:3000/setFocus'); + await page.locator('button:text("Select Input Content")').click(); + await page.locator('input[name="selectInputContent"]').fill('New Value'); + await expect(page.locator('input[name="selectInputContent"]')).toHaveValue( + 'New Value', + ); + }); + + test('should focus textarea', async ({ page }) => { + await page.goto('http://localhost:3000/setFocus'); + await page.locator('button:text("Focus Textarea")').click(); + await expect(page.locator('textarea[name="focusTextarea"]')).toHaveFocus(); + }); + + test('should select input content1', async ({ page }) => { + await page.goto('http://localhost:3000/setFocus'); + await page.locator('button:text("Select Textarea Content")').click(); + await page + .locator('textarea[name="selectTextareaContent"]') + .fill('New Value'); + await expect( + page.locator('textarea[name="selectTextareaContent"]'), + ).toHaveValue('New Value'); + }); +}); diff --git a/playwright/e2e/setValue.spec.ts b/playwright/e2e/setValue.spec.ts new file mode 100644 index 00000000000..fd7551ce83e --- /dev/null +++ b/playwright/e2e/setValue.spec.ts @@ -0,0 +1,73 @@ +import { expect, test } from '@playwright/test'; + +test.describe('form setValue', () => { + test('should set input value, trigger validation and clear all errors', async ({ + page, + }) => { + await page.goto('http://localhost:3000/setValue'); + + await expect(page.locator('input[name="firstName"]')).toHaveValue('wrong'); + await expect(page.locator('input[name="age"]')).toHaveValue('2'); + await expect(page.locator('input[name="array.0"]')).toHaveValue('array.0'); + await expect(page.locator('input[name="array.1"]')).toHaveValue('array.1'); + await expect(page.locator('input[name="array.2"]')).toHaveValue('array.2'); + await expect(page.locator('input[name="object.firstName"]')).toHaveValue( + 'firstName', + ); + await expect(page.locator('input[name="object.lastName"]')).toHaveValue( + 'lastName', + ); + await expect(page.locator('input[name="object.middleName"]')).toHaveValue( + 'middleName', + ); + await expect(page.locator('input[name="radio"]')).toBeChecked(); + await expect( + page.locator('input[name="checkboxArray"][value="2"]'), + ).toBeChecked(); + await expect( + page.locator('input[name="checkboxArray"][value="3"]'), + ).toBeChecked(); + await expect(page.locator('select[name="select"]')).toHaveValue('a'); + await expect(page.locator('select[name="multiple"]')).toHaveValue([ + 'a', + 'b', + ]); + await expect(page.locator('#trigger')).toHaveText('Trigger error'); + await expect(page.locator('#lastName')).not.toBeVisible(); + await expect(page.locator('#nestedValue')).toHaveText('required'); + + await page.locator('#submit').click(); + + await expect(page.locator('#lastName')).toHaveText('Last name error'); + + await page.locator('input[name="lastName"]').fill('test'); + await page.locator('input[name="trigger"]').fill('trigger'); + await page.locator('input[name="nestedValue"]').fill('test'); + + await page.locator('#submit').click(); + await expect(page.locator('p')).toHaveCount(0); + await expect(page.locator('#renderCount')).toHaveText('9'); + + await page.locator('#setMultipleValues').click(); + await expect(page.locator('input[name="array.0"]')).toHaveValue( + 'array[0]1', + ); + await expect(page.locator('input[name="array.1"]')).toHaveValue( + 'array[1]1', + ); + await expect(page.locator('input[name="array.2"]')).toHaveValue( + 'array[2]1', + ); + await expect(page.locator('input[name="object.firstName"]')).toHaveValue( + 'firstName1', + ); + await expect(page.locator('input[name="object.lastName"]')).toHaveValue( + 'lastName1', + ); + await expect(page.locator('input[name="object.middleName"]')).toHaveValue( + 'middleName1', + ); + await expect(page.locator('input[name="nestedValue"]')).toHaveValue('a,b'); + await expect(page.locator('#renderCount')).toHaveText('9'); + }); +}); diff --git a/playwright/e2e/setValueAsyncStrictMode.spec.ts b/playwright/e2e/setValueAsyncStrictMode.spec.ts new file mode 100644 index 00000000000..abaf140945d --- /dev/null +++ b/playwright/e2e/setValueAsyncStrictMode.spec.ts @@ -0,0 +1,13 @@ +import { expect, test } from '@playwright/test'; + +test.describe('form setValueAsyncStrictMode', () => { + test('should set async input value correctly', async ({ page }) => { + await page.goto('http://localhost:3000/setValueAsyncStrictMode'); + + await page.waitForTimeout(10); + + await page.locator('#submit').click(); + + await expect(page.locator('p')).toHaveText('["test","A","B","C","D"]'); + }); +}); diff --git a/playwright/e2e/setValueCustomRegister.spec.ts b/playwright/e2e/setValueCustomRegister.spec.ts new file mode 100644 index 00000000000..7ba4c92b637 --- /dev/null +++ b/playwright/e2e/setValueCustomRegister.spec.ts @@ -0,0 +1,33 @@ +import { expect, test } from '@playwright/test'; + +test.describe('setValue with react native or web', () => { + test('should only trigger re-render when form state changed or error triggered', async ({ + page, + }) => { + await page.goto('http://localhost:3000/setValueCustomRegister'); + await expect(page.locator('#dirty')).toHaveText('false'); + await page.locator('#TriggerDirty').click(); + await expect(page.locator('#dirty')).toHaveText('true'); + await page.locator('#TriggerNothing').click(); + await page.locator('#TriggerNothing').click(); + await page.locator('#TriggerNothing').click(); + await page.locator('#TriggerNothing').click(); + await expect(page.locator('#renderCount')).toHaveText('2'); + + await page.locator('#WithError').click(); + await expect(page.locator('#renderCount')).toHaveText('3'); + await page.locator('#WithError').click(); + await expect(page.locator('#renderCount')).toHaveText('4'); + + await page.locator('#WithoutError').click(); + await expect(page.locator('#renderCount')).toHaveText('5'); + await page.locator('#WithoutError').click(); + await expect(page.locator('#renderCount')).toHaveText('6'); + + await page.locator('#WithError').click(); + await expect(page.locator('#renderCount')).toHaveText('7'); + + await page.locator('#TriggerNothing').click(); + await expect(page.locator('#renderCount')).toHaveText('7'); + }); +}); diff --git a/playwright/e2e/setValueWithSchema.spec.ts b/playwright/e2e/setValueWithSchema.spec.ts new file mode 100644 index 00000000000..f8534e2a4e5 --- /dev/null +++ b/playwright/e2e/setValueWithSchema.spec.ts @@ -0,0 +1,39 @@ +import { expect, test } from '@playwright/test'; + +test.describe('form setValue with schema', () => { + test('should set input value, trigger validation and clear all errors', async ({ + page, + }) => { + await page.goto('http://localhost:3000/setValueWithSchema'); + + await page.locator('input[name="firstName"]').fill('a'); + await expect(page.locator('input[name="firstName"] + p')).toHaveText( + 'firstName error', + ); + await expect(page.locator('p')).toHaveCount(1); + await page.locator('input[name="firstName"]').fill('asdasdasdasd'); + + await page.locator('input[name="lastName"]').fill('a'); + await expect(page.locator('input[name="lastName"] + p')).toHaveText( + 'lastName error', + ); + await expect(page.locator('p')).toHaveCount(1); + await page.locator('input[name="lastName"]').fill('asdasdasdasd'); + + await page.locator('input[name="age"]').fill('a2323'); + + await page.locator('#submit').click(); + await expect(page.locator('p')).toHaveCount(1); + await expect(page.locator('input[name="requiredField"] + p')).toHaveText( + 'RequiredField error', + ); + + await page.locator('#setValue').click(); + await expect(page.locator('input[name="requiredField"]')).toHaveValue( + 'test123456789', + ); + await expect(page.locator('p')).toHaveCount(0); + + await expect(page.locator('#renderCount')).toHaveText('34'); + }); +}); diff --git a/playwright/e2e/setValueWithTrigger.spec.ts b/playwright/e2e/setValueWithTrigger.spec.ts new file mode 100644 index 00000000000..5db11eda291 --- /dev/null +++ b/playwright/e2e/setValueWithTrigger.spec.ts @@ -0,0 +1,31 @@ +import { expect, test } from '@playwright/test'; + +test.describe('form setValue with trigger', () => { + test('should set input value and trigger validation', async ({ page }) => { + await page.goto('http://localhost:3000/setValueWithTrigger'); + + await page.locator('input[name="firstName"]').fill('a'); + await expect(page.locator('input[name="firstName"] + p')).toHaveText( + 'minLength 10', + ); + await page.locator('input[name="firstName"]').fill(''); + await expect(page.locator('input[name="firstName"] + p')).toHaveText( + 'required', + ); + await page.locator('input[name="firstName"]').fill('clear1234567'); + + await page.locator('input[name="lastName"]').fill('a'); + await expect(page.locator('input[name="lastName"] + p')).toHaveText( + 'too short', + ); + await page.locator('input[name="lastName"]').fill('fsdfsdfsd'); + await expect(page.locator('input[name="lastName"] + p')).toHaveText( + 'error message', + ); + await page.locator('input[name="lastName"]').fill(''); + await page.locator('input[name="lastName"]').fill('bill'); + + await expect(page.locator('p')).toHaveCount(0); + await expect(page.locator('#renderCount')).toHaveText('30'); + }); +}); diff --git a/playwright/e2e/triggerValidation.spec.ts b/playwright/e2e/triggerValidation.spec.ts new file mode 100644 index 00000000000..315adb0fbd6 --- /dev/null +++ b/playwright/e2e/triggerValidation.spec.ts @@ -0,0 +1,24 @@ +import { expect, test } from '@playwright/test'; + +test.describe('form trigger', () => { + test('should trigger input validation', async ({ page }) => { + await page.goto('http://localhost:3000/trigger-validation'); + + await expect(page.locator('#testError')).toBeEmpty(); + await expect(page.locator('#test1Error')).toBeEmpty(); + await expect(page.locator('#test2Error')).toBeEmpty(); + + await page.locator('#single').click(); + await expect(page.locator('#testError')).toHaveText('required'); + await page.locator('#single').click(); + + await page.locator('#multiple').click(); + await expect(page.locator('#test1Error')).toHaveText('required'); + await expect(page.locator('#test2Error')).toHaveText('required'); + + await expect(page.locator('#renderCount')).toHaveText('4'); + + await page.locator('#multiple').click(); + await expect(page.locator('#renderCount')).toHaveText('5'); + }); +}); diff --git a/playwright/e2e/useFieldArray.spec.ts b/playwright/e2e/useFieldArray.spec.ts new file mode 100644 index 00000000000..e209733fd27 --- /dev/null +++ b/playwright/e2e/useFieldArray.spec.ts @@ -0,0 +1,132 @@ +import { expect, test } from '@playwright/test'; + +test.describe('useFieldArray', () => { + test('should behaviour correctly without defaultValues', async ({ page }) => { + await page.goto('http://localhost:3000/useFieldArray/normal'); + + await page.locator('#append').click(); + await expect(page.locator('ul > li')).toHaveCount(1); + + await page.locator('#submit').click(); + await expect(page.locator('#result')).toContainText( + JSON.stringify({ + data: [{ name: '2' }], + }), + ); + + await page.locator('#prepend').click(); + await expect(page.locator('ul > li')).toHaveCount(2); + + // await expect(page.locator('ul > li').nth(0)).toContainText('7'); + + await page.locator('#append').click(); + await expect(page.locator('ul > li')).toHaveCount(3); + + // await expect(page.locator('ul > li').nth(2)).toContainText('9'); + + await page.locator('#submit').click(); + await expect(page.locator('#result')).toContainText( + JSON.stringify({ + data: [{ name: '7' }, { name: '2' }, { name: '9' }], + }), + ); + + await page.locator('#swap').click(); + // await expect(page.locator('ul > li').nth(1)).toContainText('9'); + // await expect(page.locator('ul > li').nth(2)).toContainText('2'); + + await page.locator('#submit').click(); + await expect(page.locator('#result')).toContainText( + JSON.stringify({ + data: [{ name: '7' }, { name: '9' }, { name: '2' }], + }), + ); + + await page.locator('#move').click(); + // await expect(page.locator('ul > li').nth(0)).toContainText('2'); + // await expect(page.locator('ul > li').nth(1)).toContainText('7'); + + await page.locator('#submit').click(); + await expect(page.locator('#result')).toContainText( + JSON.stringify({ + data: [{ name: '2' }, { name: '7' }, { name: '9' }], + }), + ); + + await page.locator('#insert').click(); + // await expect(page.locator('ul > li').nth(1)).toContainText('22'); + + await page.locator('#submit').click(); + await expect(page.locator('#result')).toContainText( + JSON.stringify({ + data: [{ name: '2' }, { name: '22' }, { name: '7' }, { name: '9' }], + }), + ); + + await page.locator('#remove').click(); + // await expect(page.locator('ul > li').nth(0)).toContainText('2'); + // await expect(page.locator('ul > li').nth(1)).toContainText('7'); + + await page.locator('#submit').click(); + await expect(page.locator('#result')).toContainText( + JSON.stringify({ + data: [{ name: '2' }, { name: '7' }, { name: '9' }], + }), + ); + + await page.locator('#delete1').click(); + + await expect(page.locator('ul > li')).toHaveCount(2); + + // await expect(page.locator('ul > li').nth(0)).toContainText('2'); + // await expect(page.locator('ul > li').nth(1)).toContainText('9'); + + await page.locator('#delete1').click(); + + await expect(page.locator('ul > li')).toHaveCount(1); + + // await expect(page.locator('ul > li').nth(0)).toContainText('2'); + + await page.locator('#submit').click(); + await expect(page.locator('#result')).toContainText( + JSON.stringify({ + data: [{ name: '2' }], + }), + ); + + await page.locator('#update').click(); + + // await expect(page.locator('ul > li').nth(0)).toContainText('changed'); + + await page.locator('#removeAll').click(); + await expect(page.locator('ul > li')).toHaveCount(0); + + await page.locator('#submit').click(); + await expect(page.locator('#result')).toContainText( + JSON.stringify({ + data: [], + }), + ); + + await page.locator('#append').click(); + await page.locator('#append').click(); + await page.locator('#append').click(); + + await page.locator('#removeAsync').click(); + await page.locator('#removeAsync').click(); + + await expect(page.locator('input')).toHaveCount(1); + + await page.locator('#submit').click(); + + await expect(page.locator('#result')).toContainText( + JSON.stringify({ + data: [{ name: '41' }], + }), + ); + + await expect(page.locator('#renderCount')).toContainText('54'); + }); + + // ... other tests +}); diff --git a/playwright/e2e/useFieldArrayAsync.spec.ts b/playwright/e2e/useFieldArrayAsync.spec.ts new file mode 100644 index 00000000000..df4da1c3eb0 --- /dev/null +++ b/playwright/e2e/useFieldArrayAsync.spec.ts @@ -0,0 +1,59 @@ +import { expect, test } from '@playwright/test'; + +test.describe('useFieldArray', () => { + test('should behaviour correctly without defaultValues', async ({ page }) => { + await page.goto('http://localhost:3000/useFieldArray/normal'); + + await page.locator('#appendAsync').click(); + + await expect(page.locator(':focus')).toHaveAttribute('id', 'field0'); + + await expect(page.locator('ul > li').nth(0).locator('input')).toHaveValue( + 'appendAsync', + ); + + await expect(page.locator(':focus')).toHaveAttribute('id', 'field0'); + + await page.locator('#prependAsync').click(); + + await expect(page.locator('ul > li').nth(0).locator('input')).toHaveValue( + 'prependAsync', + ); + + await page.locator('#insertAsync').click(); + + await expect(page.locator(':focus')).toHaveAttribute('id', 'field1'); + + await expect(page.locator('#field1')).toHaveValue('insertAsync'); + + await page.locator('#swapAsync').click(); + + await expect(page.locator('#field0')).toHaveValue('insertAsync'); + await expect(page.locator('#field1')).toHaveValue('prependAsync'); + + await page.locator('#moveAsync').click(); + + await expect(page.locator('#field1')).toHaveValue('insertAsync'); + await expect(page.locator('#field0')).toHaveValue('prependAsync'); + + await page.locator('#updateAsync').click(); + + await expect(page.locator('#field0')).toHaveValue('updateAsync'); + + await page.locator('#replaceAsync').click(); + + await expect(page.locator('#field0')).toHaveValue('16. lorem'); + await expect(page.locator('#field1')).toHaveValue('16. ipsum'); + await expect(page.locator('#field2')).toHaveValue('16. dolor'); + await expect(page.locator('#field3')).toHaveValue('16. sit amet'); + + await page.locator('#removeAsync').click(); + + await page.locator('#resetAsync').click(); + + const listItems = await page.locator('ul > li'); + for (let i = 0; i < (await listItems.count()); i++) { + await expect(listItems.nth(i)).not.toBeVisible(); + } + }); +}); diff --git a/playwright/e2e/useFieldArrayNested.spec.ts b/playwright/e2e/useFieldArrayNested.spec.ts new file mode 100644 index 00000000000..48dfcfa8642 --- /dev/null +++ b/playwright/e2e/useFieldArrayNested.spec.ts @@ -0,0 +1,39 @@ +import { expect, test } from '@playwright/test'; + +test.describe('useFieldArrayNested', () => { + test('should work correctly with nested field array', async ({ page }) => { + await page.goto('http://localhost:3000/useFieldArrayNested'); + + await page.locator('#nest-append-0').click(); + await page.locator('#nest-prepend-0').click(); + await page.locator('#nest-insert-0').click(); + await page.locator('#nest-swap-0').click(); + await page.locator('#nest-move-0').click(); + + await expect( + page.locator('input[name="test.0.keyValue.0.name"]'), + ).toHaveValue('insert'); + await expect( + page.locator('input[name="test.0.keyValue.1.name"]'), + ).toHaveValue('prepend'); + await expect( + page.locator('input[name="test.0.keyValue.2.name"]'), + ).toHaveValue('1a'); + await expect( + page.locator('input[name="test.0.keyValue.3.name"]'), + ).toHaveValue('1c'); + await expect( + page.locator('input[name="test.0.keyValue.4.name"]'), + ).toHaveValue('append'); + + await page.locator('#nest-remove-0').click(); + await expect( + page.locator('input[name="test.0.keyValue.2.name"]'), + ).toHaveValue('1c'); + await expect( + page.locator('input[name="test.0.keyValue.3.name"]'), + ).toHaveValue('append'); + + // ... continue converting the rest of the test commands to Playwright + }); +}); diff --git a/playwright/e2e/useFieldArrayUnregister.spec.ts b/playwright/e2e/useFieldArrayUnregister.spec.ts new file mode 100644 index 00000000000..009f4e05778 --- /dev/null +++ b/playwright/e2e/useFieldArrayUnregister.spec.ts @@ -0,0 +1,190 @@ +import { expect, test } from '@playwright/test'; + +test.describe('useFieldArrayUnregister', () => { + test('should behaviour correctly', async ({ page }) => { + await page.goto('http://localhost:3000/UseFieldArrayUnregister'); + + await page.locator('#field0').fill('bill'); + + await page.locator('input[name="data.0.conditional"]').type('test'); + + await expect(page.locator('#dirtyFields')).toContainText( + JSON.stringify({ + data: [{ name: true, conditional: true }], + }), + ); + + await page.locator('input[name="data.0.conditional"]').press('Tab'); + + await expect(page.locator('#touched')).toContainText( + JSON.stringify([{ name: true, conditional: true }]), + ); + + await page.locator('#prepend').click(); + + await expect( + page.locator('input[name="data.0.conditional"]'), + ).not.toBeVisible(); + await expect(page.locator('input[name="data.1.conditional"]')).toHaveValue( + '', + ); + + await expect(page.locator('#dirtyFields')).toContainText( + JSON.stringify({ + data: [ + { name: true }, + { name: true, conditional: true }, + { name: true }, + { name: true }, + ], + }), + ); + + await expect(page.locator('#touched')).toContainText( + JSON.stringify([null, { name: true, conditional: true }]), + ); + + await page.locator('input[name="data.0.name"]').press('Tab'); + + await page.locator('#swap').click(); + + await expect( + page.locator('input[name="data.1.conditional"]'), + ).not.toBeVisible(); + await expect(page.locator('input[name="data.2.conditional"]')).toHaveValue( + '', + ); + + await expect(page.locator('#dirtyFields')).toContainText( + JSON.stringify({ + data: [ + { name: true }, + { name: false }, + { name: true, conditional: true }, + { name: true }, + ], + }), + ); + + await expect(page.locator('#touched')).toContainText( + JSON.stringify([{ name: true }, null, { name: true, conditional: true }]), + ); + + await page.locator('#insert').click(); + + await page.locator('#insert').click(); + + await page.locator('input[name="data.4.name"]').type('test'); + + await expect(page.locator('#dirtyFields')).toContainText( + JSON.stringify({ + data: [ + { name: true }, + { name: true }, + { name: true }, + { name: true }, + { name: true, conditional: true }, + { name: true }, + ], + }), + ); + + await expect(page.locator('#touched')).toContainText( + JSON.stringify([ + { name: true }, + { name: true }, + { name: true }, + null, + { name: true, conditional: true }, + ]), + ); + + await page.locator('#move').click(); + + await page.locator('input[name="data.2.name"]').fill('bill'); + + await expect(page.locator('input[name="data.2.conditional"]')).toHaveValue( + '', + ); + + await expect(page.locator('#dirtyFields')).toContainText( + JSON.stringify({ + data: [ + { name: true }, + { name: true }, + { name: true, conditional: true }, + { name: true }, + { name: true }, + { name: true }, + ], + }), + ); + + await expect(page.locator('#touched')).toContainText( + JSON.stringify([ + { name: true }, + { name: true }, + { name: true, conditional: true }, + { name: true }, + null, + ]), + ); + + await page.locator('#delete1').click(); + + await expect(page.locator('input[name="data.1.conditional"]')).toHaveValue( + '', + ); + + await page.locator('#submit').click(); + + await expect(page.locator('#result')).toContainText( + JSON.stringify({ + data: [ + { name: '5' }, + { name: 'bill', conditional: '' }, + { name: '10' }, + { name: 'test1' }, + { name: 'test2' }, + ], + }), + ); + + await page.locator('input[name="data.3.name"]').type('test'); + + await page.locator('#submit').click(); + + const expected = { + data: [ + { name: '5' }, + { name: 'bill', conditional: '' }, + { name: '10' }, + { name: 'test1test' }, + { name: 'test2' }, + ], + }; + + // expect(JSON.parse(await page.locator('#result').textContent())).toEqual( + // expected, + // ); + + await page.locator('#delete3').click(); + + await page.locator('#submit').click(); + + const expected1 = { + data: [ + { name: '5' }, + { name: 'bill', conditional: '' }, + { name: '10' }, + { name: 'test2' }, + ], + }; + + expect(JSON.parse(await page.locator('#result').textContent())).toEqual( + expected1, + ); + + await expect(page.locator('#renderCount')).toContainText('32'); + }); +}); diff --git a/playwright/e2e/useFormState.spec.ts b/playwright/e2e/useFormState.spec.ts new file mode 100644 index 00000000000..b9f8dc4526a --- /dev/null +++ b/playwright/e2e/useFormState.spec.ts @@ -0,0 +1,161 @@ +import { expect, test } from '@playwright/test'; + +test.describe('useFormState', () => { + test('should subscribed to the form state without re-render the root', async ({ + page, + }) => { + await page.goto('http://localhost:3000/useFormState'); + await page.locator('button#submit').click(); + + await page.locator('input[name="firstName"]').fill('billa'); + await page.locator('input[name="arrayItem.0.test1"]').fill('ab'); + await page.locator('input[name="nestItem.nest1"]').fill('ab'); + await page.locator('input[name="lastName"]').fill('luo123456'); + await page.locator('select[name="selectNumber"]').selectOption('1'); + await page.locator('input[name="pattern"]').fill('luo'); + await page.locator('input[name="min"]').fill('1'); + await page.locator('input[name="max"]').fill('21'); + await page.locator('input[name="minDate"]').fill('2019-07-30'); + await page.locator('input[name="maxDate"]').fill('2019-08-02'); + await page.locator('input[name="lastName"]').fill(''); + await page.locator('input[name="lastName"]').fill('luo'); + await page.locator('input[name="minLength"]').fill('b'); + await page.locator('input[name="minLength"]').press('Tab'); + + const state1 = JSON.parse(await page.locator('#state').textContent()); + expect(state1).toStrictEqual({ + isDirty: true, + touched: [ + 'nestItem', + 'firstName', + 'arrayItem', + 'lastName', + 'selectNumber', + 'pattern', + 'min', + 'max', + 'minDate', + 'maxDate', + 'minLength', + ], + dirty: [ + 'firstName', + 'arrayItem', + 'nestItem', + 'lastName', + 'selectNumber', + 'pattern', + 'min', + 'max', + 'minDate', + 'maxDate', + 'minLength', + ], + isSubmitted: true, + isSubmitSuccessful: false, + submitCount: 0, + isValid: false, + }); + + await page.locator('input[name="pattern"]').fill('23'); + await page.locator('input[name="minLength"]').fill('bi'); + await page.locator('input[name="minRequiredLength"]').fill('bi'); + await page.locator('input[name="min"]').fill(''); + await page.locator('input[name="min"]').fill('11'); + await page.locator('input[name="max"]').fill(''); + await page.locator('input[name="max"]').fill('19'); + await page.locator('input[name="minDate"]').fill('2019-08-01'); + await page.locator('input[name="maxDate"]').fill('2019-08-01'); + + const state2 = JSON.parse(await page.locator('#state').textContent()); + expect(state2).toStrictEqual({ + isDirty: true, + touched: [ + 'nestItem', + 'firstName', + 'arrayItem', + 'lastName', + 'selectNumber', + 'pattern', + 'min', + 'max', + 'minDate', + 'maxDate', + 'minLength', + 'minRequiredLength', + ], + dirty: [ + 'firstName', + 'arrayItem', + 'nestItem', + 'lastName', + 'selectNumber', + 'pattern', + 'min', + 'max', + 'minDate', + 'maxDate', + 'minLength', + 'minRequiredLength', + ], + isSubmitted: true, + isSubmitSuccessful: false, + submitCount: 0, + isValid: true, + }); + + await page.locator('#submit').click(); + + const state3 = JSON.parse(await page.locator('#state').textContent()); + expect(state3).toStrictEqual({ + isDirty: true, + touched: [ + 'nestItem', + 'firstName', + 'arrayItem', + 'lastName', + 'selectNumber', + 'pattern', + 'min', + 'max', + 'minDate', + 'maxDate', + 'minLength', + 'minRequiredLength', + ], + dirty: [ + 'firstName', + 'arrayItem', + 'nestItem', + 'lastName', + 'selectNumber', + 'pattern', + 'min', + 'max', + 'minDate', + 'maxDate', + 'minLength', + 'minRequiredLength', + ], + isSubmitted: true, + isSubmitSuccessful: true, + submitCount: 1, + isValid: true, + }); + + await page.locator('#resetForm').click(); + + const state4 = JSON.parse(await page.locator('#state').textContent()); + expect(state4).toStrictEqual({ + isDirty: false, + touched: [], + dirty: [], + isSubmitted: false, + isSubmitSuccessful: false, + submitCount: 0, + isValid: true, + }); + + await expect(page.locator('#renderCount')).toHaveText('1'); + }); +}); diff --git a/playwright/e2e/useWatch.spec.ts b/playwright/e2e/useWatch.spec.ts new file mode 100644 index 00000000000..7749f2cb7de --- /dev/null +++ b/playwright/e2e/useWatch.spec.ts @@ -0,0 +1,65 @@ +import { expect, test } from '@playwright/test'; + +test.describe('useWatch', () => { + test('should only trigger render when interact with input 1', async ({ + page, + }) => { + await page.goto('http://localhost:3000/useWatch'); + await page.locator('input[name="test"]').fill('t'); + + await expect(page.locator('#parentCounter')).toHaveText('1'); + await expect(page.locator('#childCounter')).toHaveText('1'); + await expect(page.locator('#grandChildCounter')).toHaveText('2'); + await expect(page.locator('#grandChild1Counter')).toHaveText('2'); + await expect(page.locator('#grandChild2Counter')).toHaveText('2'); + await expect(page.locator('#grandchild01')).toHaveText('t'); + await expect(page.locator('#grandchild00')).toHaveText('t'); + + await page.locator('input[name="test"]').fill('h'); + await expect(page.locator('#grandchild00')).toHaveText('th'); + await expect(page.locator('#grandchild01')).toHaveText('th'); + await expect(page.locator('#grandchild2')).toHaveText('t'); + }); + + test('should only trigger render when interact with input 2', async ({ + page, + }) => { + await page.goto('http://localhost:3000/useWatch'); + await page.locator('input[name="test1"]').fill('h'); + + await expect(page.locator('#parentCounter')).toHaveText('1'); + await expect(page.locator('#childCounter')).toHaveText('1'); + await expect(page.locator('#grandChildCounter')).toHaveText('2'); + await expect(page.locator('#grandChild1Counter')).toHaveText('2'); + await expect(page.locator('#grandChild2Counter')).toHaveText('2'); + + await page.locator('input[name="test1"]').fill('h'); + await page.locator('input[name="test"]').fill('h'); + await expect(page.locator('#grandchild00')).toHaveText('h'); + await expect(page.locator('#grandchild01')).toHaveText('h'); + await expect(page.locator('#grandchild1')).toHaveText('hh'); + await expect(page.locator('#grandchild2')).toHaveText('hhh'); + }); + + test('should only trigger render when interact with input 3', async ({ + page, + }) => { + await page.goto('http://localhost:3000/useWatch'); + await page.locator('input[name="test2"]').fill('e'); + + await expect(page.locator('#parentCounter')).toHaveText('1'); + await expect(page.locator('#childCounter')).toHaveText('1'); + await expect(page.locator('#grandChildCounter')).toHaveText('2'); + await expect(page.locator('#grandChild1Counter')).toHaveText('2'); + await expect(page.locator('#grandChild2Counter')).toHaveText('2'); + + await page.locator('input[name="test2"]').fill('eh'); + + await page.locator('input[name="test1"]').fill('eh'); + await page.locator('input[name="test"]').fill('eh'); + await expect(page.locator('#grandchild00')).toHaveText('eh'); + await expect(page.locator('#grandchild01')).toHaveText('eh'); + await expect(page.locator('#grandchild1')).toHaveText('eh'); + await expect(page.locator('#grandchild2')).toHaveText('eheheeh'); + }); +}); diff --git a/playwright/e2e/useWatchUseFieldArrayNested.spec.ts b/playwright/e2e/useWatchUseFieldArrayNested.spec.ts new file mode 100644 index 00000000000..938e60fd93d --- /dev/null +++ b/playwright/e2e/useWatchUseFieldArrayNested.spec.ts @@ -0,0 +1,225 @@ +import { expect, test } from '@playwright/test'; + +test.describe('useWatchUseFieldArrayNested', () => { + test('should watch the correct nested field array', async ({ page }) => { + await page.goto('http://localhost:3000/useWatchUseFieldArrayNested'); + + const expected = [ + { + firstName: 'Bill', + keyValue: [{ name: '1a' }, { name: '1c' }], + lastName: 'Luo', + }, + ]; + + // Get the content of the #result element. + const resultContent = await page.locator('#result').textContent(); + + // Parse the content to a JavaScript object. + const received = JSON.parse(resultContent); + + // Now you can compare using the regular expect function. + expect(received).toEqual(expected); + + await page.locator('#nest-append-0').click(); + await page.locator('#nest-prepend-0').click(); + await page.locator('#nest-insert-0').click(); + await page.locator('#nest-swap-0').click(); + await page.locator('#nest-move-0').click(); + + const expected2 = [ + { + firstName: 'Bill', + keyValue: [ + { name: 'insert' }, + { name: 'prepend' }, + { name: '1a' }, + { name: '1c' }, + { name: 'append' }, + ], + lastName: 'Luo', + }, + ]; + const resultContent2 = await page.locator('#result').textContent(); + + const received2 = JSON.parse(resultContent2); + + expect(received2).toEqual(expected2); + + await page.locator('#nest-remove-0').click(); + + await page.locator('#submit').click(); + + const expected3 = [ + { + firstName: 'Bill', + keyValue: [ + { name: 'insert' }, + { name: '1a' }, + { name: '1c' }, + { name: 'append' }, + ], + lastName: 'Luo', + }, + ]; + + expect(JSON.parse(await page.locator('#result').textContent())).toEqual( + expected3, + ); + + await page.locator('#prepend').click(); + await page.locator('#append').click(); + await page.locator('#swap').click(); + await page.locator('#insert').click(); + + const expected4 = [ + { firstName: 'prepend', keyValue: [] }, + { firstName: 'insert', keyValue: [] }, + { firstName: 'append', keyValue: [] }, + { + firstName: 'Bill', + keyValue: [ + { name: 'insert' }, + { name: '1a' }, + { name: '1c' }, + { name: 'append' }, + ], + lastName: 'Luo', + }, + ]; + + expect(JSON.parse(await page.locator('#result').textContent())).toEqual( + expected4, + ); + + await page.locator('#nest-append-0').click(); + await page.locator('#nest-prepend-0').click(); + await page.locator('#nest-insert-0').click(); + await page.locator('#nest-swap-0').click(); + await page.locator('#nest-move-0').click(); + + const expected5 = [ + { + firstName: 'prepend', + keyValue: [{ name: 'insert' }, { name: 'prepend' }, { name: 'append' }], + }, + { firstName: 'insert', keyValue: [] }, + { firstName: 'append', keyValue: [] }, + { + firstName: 'Bill', + lastName: 'Luo', + keyValue: [ + { name: 'insert' }, + { name: '1a' }, + { name: '1c' }, + { name: 'append' }, + ], + }, + ]; + + expect(JSON.parse(await page.locator('#result').textContent())).toEqual( + expected5, + ); + + await page.locator('#nest-update-3').click(); + + await expect( + page.locator('input[name="test.3.keyValue.2.name"]'), + ).toHaveValue('update'); + + const expected6 = [ + { + firstName: 'prepend', + keyValue: [{ name: 'insert' }, { name: 'prepend' }, { name: 'update' }], + }, + { firstName: 'insert', keyValue: [] }, + { firstName: 'append', keyValue: [] }, + { + firstName: 'Bill', + keyValue: [ + { name: 'insert' }, + { name: '1a' }, + { name: 'update' }, + { name: 'append' }, + ], + lastName: 'Luo', + }, + ]; + + // expect(JSON.parse(await page.locator('#result').textContent())).toEqual( + // expected6, + // ); + + await page.locator('#nest-update-0').click(); + + const expected7 = [ + { + firstName: 'prepend', + keyValue: [{ name: 'insert' }, { name: 'prepend' }, { name: 'update' }], + }, + { firstName: 'insert', keyValue: [] }, + { firstName: 'append', keyValue: [] }, + { + firstName: 'Bill', + lastName: 'Luo', + keyValue: [ + { name: 'insert' }, + { name: '1a' }, + { name: 'update' }, + { name: 'append' }, + ], + }, + ]; + + expect(JSON.parse(await page.locator('#result').textContent())).toEqual( + expected7, + ); + + await page.locator('#nest-remove-3').click(); + await page.locator('#nest-remove-3').click(); + + const expected8 = [ + { + firstName: 'prepend', + keyValue: [{ name: 'insert' }, { name: 'prepend' }, { name: 'update' }], + }, + { firstName: 'insert', keyValue: [] }, + { firstName: 'append', keyValue: [] }, + { + firstName: 'Bill', + lastName: 'Luo', + keyValue: [{ name: 'insert' }, { name: 'append' }], + }, + ]; + + expect(JSON.parse(await page.locator('#result').textContent())).toEqual( + expected8, + ); + + await page.locator('#nest-remove-all-3').click(); + await page.locator('#nest-remove-all-2').click(); + await page.locator('#nest-remove-all-1').click(); + await page.locator('#nest-remove-all-0').click(); + + const expected9 = [ + { firstName: 'prepend', keyValue: [] }, + { firstName: 'insert', keyValue: [] }, + { firstName: 'append', keyValue: [] }, + { firstName: 'Bill', lastName: 'Luo', keyValue: [] }, + ]; + + expect(JSON.parse(await page.locator('#result').textContent())).toEqual( + expected9, + ); + + await page.locator('#remove').click(); + await page.locator('#remove').click(); + await page.locator('#remove').click(); + + await expect(page.locator('#result')).toContainText( + JSON.stringify([{ firstName: 'prepend', keyValue: [] }]), + ); + + await expect(page.locator('#count')).toContainText('8'); + }); +}); diff --git a/playwright/e2e/validateFieldCriteria.spec.ts b/playwright/e2e/validateFieldCriteria.spec.ts new file mode 100644 index 00000000000..e509de83890 --- /dev/null +++ b/playwright/e2e/validateFieldCriteria.spec.ts @@ -0,0 +1,106 @@ +import { expect, test } from '@playwright/test'; + +test.describe('validate field criteria', () => { + test('should validate the form, show all errors and clear all', async ({ + page, + }) => { + await page.goto('http://localhost:3000/validate-field-criteria'); + await page.locator('button#submit').click(); + await expect(page.locator('input[name="firstName"] + p')).toHaveText( + 'firstName required', + ); + await page.locator('input[name="firstName"]').fill('te'); + await expect(page.locator('input[name="firstName"] + p')).toHaveText( + 'firstName minLength', + ); + await page.locator('input[name="firstName"]').fill('testtesttest'); + + await expect(page.locator('input[name="min"] + p')).toHaveText( + 'min required', + ); + await page.locator('input[name="min"]').fill('2'); + await expect(page.locator('input[name="min"] + p')).toHaveText('min min'); + await page.locator('input[name="min"]').fill('32'); + await expect(page.locator('input[name="min"] + p')).toHaveText('min max'); + await page.locator('input[name="min"]').fill(''); + await page.locator('input[name="min"]').fill('10'); + + await expect(page.locator('input[name="minDate"] + p')).toHaveText( + 'minDate required', + ); + await page.locator('input[name="minDate"]').fill('2019-07-01'); + await expect(page.locator('input[name="minDate"] + p')).toHaveText( + 'minDate min', + ); + await page.locator('input[name="minDate"]').fill('2019-08-01'); + + await expect(page.locator('input[name="maxDate"] + p')).toHaveText( + 'maxDate required', + ); + await page.locator('input[name="maxDate"]').fill('2019-09-01'); + await expect(page.locator('input[name="maxDate"] + p')).toHaveText( + 'maxDate max', + ); + await page.locator('input[name="maxDate"]').fill('2019-08-01'); + + await expect(page.locator('input[name="minLength"] + p')).toHaveText( + 'minLength required', + ); + await page.locator('input[name="minLength"]').fill('1'); + await expect(page.locator('input[name="minLength"] + p')).toHaveText( + 'minLength minLength', + ); + await page.locator('input[name="minLength"]').fill('12'); + + await expect(page.locator('select[name="selectNumber"] + p')).toHaveText( + 'selectNumber required', + ); + await page.locator('select[name="selectNumber"]').selectOption('12'); + + await expect(page.locator('input[name="pattern"] + p')).toHaveText( + 'pattern required', + ); + await page.locator('input[name="pattern"]').fill('t'); + await expect(page.locator('input[name="pattern"] + p')).toHaveText( + 'pattern pattern', + ); + await expect(page.locator('input[name="pattern"] + p + p')).toHaveText( + 'pattern minLength', + ); + await page.locator('input[name="pattern"]').fill(''); + await page.locator('input[name="pattern"]').fill('12345'); + + await expect(page.locator('select[name="multiple"] + p')).toHaveText( + 'multiple required', + ); + await expect(page.locator('select[name="multiple"] + p + p')).toHaveText( + 'multiple validate', + ); + await page + .locator('select[name="multiple"]') + .selectOption(['optionA', 'optionB']); + + await expect(page.locator('input[name="validate"] + p')).toHaveText( + 'validate test', + ); + await expect(page.locator('input[name="validate"] + p + p')).toHaveText( + 'validate test1', + ); + await expect(page.locator('input[name="validate"] + p + p + p')).toHaveText( + 'validate test2', + ); + await page.locator('input[name="validate"]').fill('test'); + + await expect(page.locator('p')).toHaveCount(0); + + await page.locator('#trigger').click(); + await expect(page.locator('p')).toHaveCount(2); + await expect(page.locator('b')).toHaveCount(2); + + await page.locator('#clear').click(); + await expect(page.locator('p')).toHaveCount(0); + await expect(page.locator('b')).toHaveCount(0); + + await expect(page.locator('#renderCount')).toHaveText('27'); + }); +}); diff --git a/playwright/e2e/watch.spec.ts b/playwright/e2e/watch.spec.ts new file mode 100644 index 00000000000..30db3a47530 --- /dev/null +++ b/playwright/e2e/watch.spec.ts @@ -0,0 +1,74 @@ +import { expect, test } from '@playwright/test'; + +test.describe('watch form validation', () => { + test('should watch all inputs', async ({ page }) => { + await page.goto('http://localhost:3000/watch'); + + await expect(page.locator('#watchAll')).toContainText('{}'); + + await expect(page.locator('#HideTestSingle')).not.toBeVisible(); + await page.locator('input[name="testSingle"]').fill('testSingle'); + await expect(page.locator('#HideTestSingle')).toContainText( + 'Hide Content TestSingle', + ); + await expect(page.locator('#watchAll')).toContainText( + JSON.stringify({ + testSingle: 'testSingle', + test: ['', ''], + testObject: { firstName: '', lastName: '' }, + toggle: false, + }), + ); + + await page.locator('input[name="test.0"]').fill('bill'); + await page.locator('input[name="test.1"]').fill('luo'); + await expect(page.locator('#testData')).toContainText('["bill","luo"]'); + await expect(page.locator('#testArray')).toContainText( + JSON.stringify(['bill', 'luo']), + ); + + await expect(page.locator('#watchAll')).toContainText( + JSON.stringify({ + testSingle: 'testSingle', + test: ['bill', 'luo'], + testObject: { firstName: '', lastName: '' }, + toggle: false, + }), + ); + + await page.locator('input[name="testObject.firstName"]').fill('bill'); + await page.locator('input[name="testObject.lastName"]').fill('luo'); + await expect(page.locator('#testObject')).toContainText( + JSON.stringify({ + firstName: 'bill', + lastName: 'luo', + }), + ); + + await expect(page.locator('#testArray')).toContainText( + JSON.stringify(['bill', 'luo']), + ); + + await expect(page.locator('#watchAll')).toContainText( + JSON.stringify({ + testSingle: 'testSingle', + test: ['bill', 'luo'], + testObject: { firstName: 'bill', lastName: 'luo' }, + toggle: false, + }), + ); + + await expect(page.locator('#hideContent')).not.toBeVisible(); + await page.locator('input[name="toggle"]').check(); + await expect(page.locator('#hideContent')).toContainText('Hide Content'); + + await expect(page.locator('#watchAll')).toContainText( + JSON.stringify({ + testSingle: 'testSingle', + test: ['bill', 'luo'], + testObject: { firstName: 'bill', lastName: 'luo' }, + toggle: true, + }), + ); + }); +}); diff --git a/playwright/e2e/watchDefaultValues.spec.ts b/playwright/e2e/watchDefaultValues.spec.ts new file mode 100644 index 00000000000..13d508d7e63 --- /dev/null +++ b/playwright/e2e/watchDefaultValues.spec.ts @@ -0,0 +1,20 @@ +import { expect, test } from '@playwright/test'; + +test.describe('watchDefaultValues', () => { + test('should return default value with watch', async ({ page }) => { + await page.goto('http://localhost:3000/watch-default-values'); + + await expect(page.locator('#watchAll')).toHaveText( + '{"test":"test","test1":{"firstName":"firstName","lastName":["lastName0","lastName1"],"deep":{"nest":"nest"}},"flatName[1]":{"whatever":"flat"}}', + ); + await expect(page.locator('#array')).toHaveText( + '["test",{"whatever":"flat"}]', + ); + await expect(page.locator('#getArray')).toHaveText( + '["lastName0","lastName1"]', + ); + await expect(page.locator('#object')).toHaveText('["test","firstName"]'); + await expect(page.locator('#single')).toHaveText('"firstName"'); + await expect(page.locator('#singleDeepArray')).toHaveText('"lastName0"'); + }); +}); diff --git a/playwright/e2e/watchUseFieldArray.spec.ts b/playwright/e2e/watchUseFieldArray.spec.ts new file mode 100644 index 00000000000..571de2c1954 --- /dev/null +++ b/playwright/e2e/watchUseFieldArray.spec.ts @@ -0,0 +1,64 @@ +import { expect, test } from '@playwright/test'; + +test.describe('watchUseFieldArray', () => { + test('should behaviour correctly when watching the field array', async ({ + page, + }) => { + await page.goto('http://localhost:3000/watch-field-array/normal'); + + await page.locator('#append').click(); + await expect(page.locator('#result')).toContainText('[{"name":"2"}]'); + + await page.locator('#field0').type('test'); + await expect(page.locator('#result')).toContainText('[{"name":"2test"}]'); + + await page.locator('#prepend').click(); + await expect(page.locator('#result')).toContainText( + '[{"name":"8"},{"name":"2test"}]', + ); + + await page.locator('#append').click(); + await page.locator('#append').click(); + await page.locator('#append').click(); + await page.locator('#update').click(); + await expect(page.locator('#result')).toContainText( + '[{"name":"8"},{"name":"2test"},{"name":"10"},{"name":"updated value"},{"name":"14"}]', + ); + + await page.locator('#swap').click(); + await expect(page.locator('#result')).toContainText( + '[{"name":"8"},{"name":"10"},{"name":"2test"},{"name":"updated value"},{"name":"14"}]', + ); + + await page.locator('#move').click(); + await expect(page.locator('#result')).toContainText( + '[{"name":"2test"},{"name":"8"},{"name":"10"},{"name":"updated value"},{"name":"14"}]', + ); + + await page.locator('#insert').click(); + await expect(page.locator('#result')).toContainText( + '[{"name":"2test"},{"name":"22"},{"name":"8"},{"name":"10"},{"name":"updated value"},{"name":"14"}]', + ); + + await page.locator('#remove').click(); + await expect(page.locator('#result')).toContainText( + '[{"name":"2test"},{"name":"8"},{"name":"10"},{"name":"updated value"},{"name":"14"}]', + ); + + await page.locator('#removeAll').click(); + await expect(page.locator('#result')).toContainText('[]'); + await expect(page.locator('#renderCount')).toContainText('28'); + }); + + test('should return empty when items been removed and defaultValues are supplied', async ({ + page, + }) => { + await page.goto('http://localhost:3000/watch-field-array/default'); + + await page.locator('#delete0').click(); + await page.locator('#delete0').click(); + await page.locator('#delete0').click(); + + await expect(page.locator('#result')).toContainText('[]'); + }); +}); diff --git a/playwright/e2e/watchUseFieldArrayNested.spec.ts b/playwright/e2e/watchUseFieldArrayNested.spec.ts new file mode 100644 index 00000000000..7ec989817d8 --- /dev/null +++ b/playwright/e2e/watchUseFieldArrayNested.spec.ts @@ -0,0 +1,204 @@ +import { expect, test } from '@playwright/test'; + +test.describe('watchUseFieldArrayNested', () => { + test('should watch the correct nested field array', async ({ page }) => { + await page.goto('http://localhost:3000/watchUseFieldArrayNested'); + + const expected = [ + { + firstName: 'Bill', + keyValue: [{ name: '1a' }, { name: '1c' }], + lastName: 'Luo', + }, + ]; + + expect(JSON.parse(await page.locator('#result').textContent())).toEqual( + expected, + ); + + await page.locator(`#nest-append-0`).click(); + await page.locator(`#nest-prepend-0`).click(); + await page.locator(`#nest-insert-0`).click(); + await page.locator(`#nest-swap-0`).click(); + await page.locator(`#nest-move-0`).click(); + + const expected1 = [ + { + firstName: 'Bill', + keyValue: [ + { name: 'insert' }, + { name: 'prepend' }, + { name: '1a' }, + { name: '1c' }, + { name: 'append' }, + ], + lastName: 'Luo', + }, + ]; + + expect(JSON.parse(await page.locator('#result').textContent())).toEqual( + expected1, + ); + + await page.locator('#nest-update-0').click(); + + const expected2 = [ + { + firstName: 'Bill', + keyValue: [ + { name: 'billUpdate' }, + { name: 'prepend' }, + { name: '1a' }, + { name: '1c' }, + { name: 'append' }, + ], + lastName: 'Luo', + }, + ]; + + expect(JSON.parse(await page.locator('#result').textContent())).toEqual( + expected2, + ); + + await page.locator(`#nest-remove-0`).click(); + + await page.locator('#submit').click(); + + const expected9 = [ + { + firstName: 'Bill', + keyValue: [ + { name: 'billUpdate' }, + { name: '1a' }, + { name: '1c' }, + { name: 'append' }, + ], + lastName: 'Luo', + }, + ]; + + expect(JSON.parse(await page.locator('#result').textContent())).toEqual( + expected9, + ); + + await page.locator('#prepend').click(); + await page.locator('#append').click(); + await page.locator('#swap').click(); + await page.locator('#insert').click(); + + const expected3 = [ + { firstName: 'prepend', keyValue: [] }, + { firstName: 'insert', keyValue: [] }, + { firstName: 'append', keyValue: [] }, + { + firstName: 'Bill', + lastName: 'Luo', + keyValue: [ + { name: 'billUpdate' }, + { name: '1a' }, + { name: '1c' }, + { name: 'append' }, + ], + }, + ]; + + expect(JSON.parse(await page.locator('#result').textContent())).toEqual( + expected3, + ); + + await page.locator(`#nest-append-0`).click(); + await page.locator(`#nest-prepend-0`).click(); + await page.locator(`#nest-insert-0`).click(); + await page.locator(`#nest-swap-0`).click(); + await page.locator(`#nest-move-0`).click(); + + const expected4 = [ + { + firstName: 'prepend', + keyValue: [{ name: 'insert' }, { name: 'prepend' }, { name: 'append' }], + }, + { firstName: 'insert', keyValue: [] }, + { firstName: 'append', keyValue: [] }, + { + firstName: 'Bill', + lastName: 'Luo', + keyValue: [ + { name: 'billUpdate' }, + { name: '1a' }, + { name: '1c' }, + { name: 'append' }, + ], + }, + ]; + + expect(JSON.parse(await page.locator('#result').textContent())).toEqual( + expected4, + ); + + await page.locator('#nest-remove-3').click(); + await page.locator('#nest-remove-3').click(); + + const expected5 = [ + { + firstName: 'prepend', + keyValue: [{ name: 'insert' }, { name: 'prepend' }, { name: 'append' }], + }, + { firstName: 'insert', keyValue: [] }, + { firstName: 'append', keyValue: [] }, + { + firstName: 'Bill', + lastName: 'Luo', + keyValue: [{ name: 'billUpdate' }, { name: 'append' }], + }, + ]; + + expect(JSON.parse(await page.locator('#result').textContent())).toEqual( + expected5, + ); + + await page.locator('#nest-remove-all-3').click(); + await page.locator('#nest-remove-all-2').click(); + await page.locator('#nest-remove-all-1').click(); + await page.locator('#nest-remove-all-0').click(); + + const expected6 = [ + { firstName: 'prepend', keyValue: [] }, + { firstName: 'insert', keyValue: [] }, + { firstName: 'append', keyValue: [] }, + { firstName: 'Bill', lastName: 'Luo', keyValue: [] }, + ]; + + expect(JSON.parse(await page.locator('#result').textContent())).toEqual( + expected6, + ); + + await page.locator('#update').click(); + + const expected7 = [ + { firstName: 'BillUpdate', keyValue: [] }, + { firstName: 'insert', keyValue: [] }, + { firstName: 'append', keyValue: [] }, + { firstName: 'Bill', lastName: 'Luo', keyValue: [] }, + ]; + + expect(JSON.parse(await page.locator('#result').textContent())).toEqual( + expected7, + ); + + await page.locator('#remove').click(); + await page.locator('#remove').click(); + await page.locator('#remove').click(); + + const expected8 = [{ firstName: 'BillUpdate', keyValue: [] }]; + + expect(JSON.parse(await page.locator('#result').textContent())).toEqual( + expected8, + ); + + await expect(page.locator('#count')).toContainText('36'); + + await page.locator('#removeAll').click(); + + await expect(page.locator('#result').textContent()).resolves.toBe('[]'); + }); +}); diff --git a/playwright/plugins/index.ts b/playwright/plugins/index.ts new file mode 100644 index 00000000000..e1052a7119f --- /dev/null +++ b/playwright/plugins/index.ts @@ -0,0 +1,17 @@ +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +module.exports = () => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config +}; diff --git a/playwright/support/commands.js b/playwright/support/commands.js new file mode 100644 index 00000000000..ca4d256f3eb --- /dev/null +++ b/playwright/support/commands.js @@ -0,0 +1,25 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add("login", (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) diff --git a/playwright/support/e2e.js b/playwright/support/e2e.js new file mode 100644 index 00000000000..57ce86eb380 --- /dev/null +++ b/playwright/support/e2e.js @@ -0,0 +1,22 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands'; + +require('@replayio/cypress/support'); + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/tests-examples/demo-todo-app.spec.ts b/tests-examples/demo-todo-app.spec.ts new file mode 100644 index 00000000000..2fd6016fe81 --- /dev/null +++ b/tests-examples/demo-todo-app.spec.ts @@ -0,0 +1,437 @@ +import { test, expect, type Page } from '@playwright/test'; + +test.beforeEach(async ({ page }) => { + await page.goto('https://demo.playwright.dev/todomvc'); +}); + +const TODO_ITEMS = [ + 'buy some cheese', + 'feed the cat', + 'book a doctors appointment' +]; + +test.describe('New Todo', () => { + test('should allow me to add todo items', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create 1st todo. + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + // Make sure the list only has one todo item. + await expect(page.getByTestId('todo-title')).toHaveText([ + TODO_ITEMS[0] + ]); + + // Create 2nd todo. + await newTodo.fill(TODO_ITEMS[1]); + await newTodo.press('Enter'); + + // Make sure the list now has two todo items. + await expect(page.getByTestId('todo-title')).toHaveText([ + TODO_ITEMS[0], + TODO_ITEMS[1] + ]); + + await checkNumberOfTodosInLocalStorage(page, 2); + }); + + test('should clear text input field when an item is added', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create one todo item. + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + // Check that input is empty. + await expect(newTodo).toBeEmpty(); + await checkNumberOfTodosInLocalStorage(page, 1); + }); + + test('should append new items to the bottom of the list', async ({ page }) => { + // Create 3 items. + await createDefaultTodos(page); + + // create a todo count locator + const todoCount = page.getByTestId('todo-count') + + // Check test using different methods. + await expect(page.getByText('3 items left')).toBeVisible(); + await expect(todoCount).toHaveText('3 items left'); + await expect(todoCount).toContainText('3'); + await expect(todoCount).toHaveText(/3/); + + // Check all items in one call. + await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS); + await checkNumberOfTodosInLocalStorage(page, 3); + }); +}); + +test.describe('Mark all as completed', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test.afterEach(async ({ page }) => { + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should allow me to mark all items as completed', async ({ page }) => { + // Complete all todos. + await page.getByLabel('Mark all as complete').check(); + + // Ensure all todos have 'completed' class. + await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + }); + + test('should allow me to clear the complete state of all items', async ({ page }) => { + const toggleAll = page.getByLabel('Mark all as complete'); + // Check and then immediately uncheck. + await toggleAll.check(); + await toggleAll.uncheck(); + + // Should be no completed classes. + await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']); + }); + + test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => { + const toggleAll = page.getByLabel('Mark all as complete'); + await toggleAll.check(); + await expect(toggleAll).toBeChecked(); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + + // Uncheck first todo. + const firstTodo = page.getByTestId('todo-item').nth(0); + await firstTodo.getByRole('checkbox').uncheck(); + + // Reuse toggleAll locator and make sure its not checked. + await expect(toggleAll).not.toBeChecked(); + + await firstTodo.getByRole('checkbox').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + + // Assert the toggle all is checked again. + await expect(toggleAll).toBeChecked(); + }); +}); + +test.describe('Item', () => { + + test('should allow me to mark items as complete', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create two items. + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + // Check first item. + const firstTodo = page.getByTestId('todo-item').nth(0); + await firstTodo.getByRole('checkbox').check(); + await expect(firstTodo).toHaveClass('completed'); + + // Check second item. + const secondTodo = page.getByTestId('todo-item').nth(1); + await expect(secondTodo).not.toHaveClass('completed'); + await secondTodo.getByRole('checkbox').check(); + + // Assert completed class. + await expect(firstTodo).toHaveClass('completed'); + await expect(secondTodo).toHaveClass('completed'); + }); + + test('should allow me to un-mark items as complete', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create two items. + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + const firstTodo = page.getByTestId('todo-item').nth(0); + const secondTodo = page.getByTestId('todo-item').nth(1); + const firstTodoCheckbox = firstTodo.getByRole('checkbox'); + + await firstTodoCheckbox.check(); + await expect(firstTodo).toHaveClass('completed'); + await expect(secondTodo).not.toHaveClass('completed'); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + await firstTodoCheckbox.uncheck(); + await expect(firstTodo).not.toHaveClass('completed'); + await expect(secondTodo).not.toHaveClass('completed'); + await checkNumberOfCompletedTodosInLocalStorage(page, 0); + }); + + test('should allow me to edit an item', async ({ page }) => { + await createDefaultTodos(page); + + const todoItems = page.getByTestId('todo-item'); + const secondTodo = todoItems.nth(1); + await secondTodo.dblclick(); + await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]); + await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter'); + + // Explicitly assert the new text value. + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + 'buy some sausages', + TODO_ITEMS[2] + ]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); +}); + +test.describe('Editing', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should hide other controls when editing', async ({ page }) => { + const todoItem = page.getByTestId('todo-item').nth(1); + await todoItem.dblclick(); + await expect(todoItem.getByRole('checkbox')).not.toBeVisible(); + await expect(todoItem.locator('label', { + hasText: TODO_ITEMS[1], + })).not.toBeVisible(); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should save edits on blur', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur'); + + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + 'buy some sausages', + TODO_ITEMS[2], + ]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); + + test('should trim entered text', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages '); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); + + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + 'buy some sausages', + TODO_ITEMS[2], + ]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); + + test('should remove the item if an empty text string was entered', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(''); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); + + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + TODO_ITEMS[2], + ]); + }); + + test('should cancel edits on escape', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape'); + await expect(todoItems).toHaveText(TODO_ITEMS); + }); +}); + +test.describe('Counter', () => { + test('should display the current number of todo items', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // create a todo count locator + const todoCount = page.getByTestId('todo-count') + + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + await expect(todoCount).toContainText('1'); + + await newTodo.fill(TODO_ITEMS[1]); + await newTodo.press('Enter'); + await expect(todoCount).toContainText('2'); + + await checkNumberOfTodosInLocalStorage(page, 2); + }); +}); + +test.describe('Clear completed button', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + }); + + test('should display the correct text', async ({ page }) => { + await page.locator('.todo-list li .toggle').first().check(); + await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible(); + }); + + test('should remove completed items when clicked', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).getByRole('checkbox').check(); + await page.getByRole('button', { name: 'Clear completed' }).click(); + await expect(todoItems).toHaveCount(2); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test('should be hidden when there are no items that are completed', async ({ page }) => { + await page.locator('.todo-list li .toggle').first().check(); + await page.getByRole('button', { name: 'Clear completed' }).click(); + await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden(); + }); +}); + +test.describe('Persistence', () => { + test('should persist its data', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + const todoItems = page.getByTestId('todo-item'); + const firstTodoCheck = todoItems.nth(0).getByRole('checkbox'); + await firstTodoCheck.check(); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + await expect(firstTodoCheck).toBeChecked(); + await expect(todoItems).toHaveClass(['completed', '']); + + // Ensure there is 1 completed item. + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + // Now reload. + await page.reload(); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + await expect(firstTodoCheck).toBeChecked(); + await expect(todoItems).toHaveClass(['completed', '']); + }); +}); + +test.describe('Routing', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + // make sure the app had a chance to save updated todos in storage + // before navigating to a new view, otherwise the items can get lost :( + // in some frameworks like Durandal + await checkTodosInLocalStorage(page, TODO_ITEMS[0]); + }); + + test('should allow me to display active items', async ({ page }) => { + const todoItem = page.getByTestId('todo-item'); + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Active' }).click(); + await expect(todoItem).toHaveCount(2); + await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test('should respect the back button', async ({ page }) => { + const todoItem = page.getByTestId('todo-item'); + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + await test.step('Showing all items', async () => { + await page.getByRole('link', { name: 'All' }).click(); + await expect(todoItem).toHaveCount(3); + }); + + await test.step('Showing active items', async () => { + await page.getByRole('link', { name: 'Active' }).click(); + }); + + await test.step('Showing completed items', async () => { + await page.getByRole('link', { name: 'Completed' }).click(); + }); + + await expect(todoItem).toHaveCount(1); + await page.goBack(); + await expect(todoItem).toHaveCount(2); + await page.goBack(); + await expect(todoItem).toHaveCount(3); + }); + + test('should allow me to display completed items', async ({ page }) => { + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Completed' }).click(); + await expect(page.getByTestId('todo-item')).toHaveCount(1); + }); + + test('should allow me to display all items', async ({ page }) => { + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Active' }).click(); + await page.getByRole('link', { name: 'Completed' }).click(); + await page.getByRole('link', { name: 'All' }).click(); + await expect(page.getByTestId('todo-item')).toHaveCount(3); + }); + + test('should highlight the currently applied filter', async ({ page }) => { + await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected'); + + //create locators for active and completed links + const activeLink = page.getByRole('link', { name: 'Active' }); + const completedLink = page.getByRole('link', { name: 'Completed' }); + await activeLink.click(); + + // Page change - active items. + await expect(activeLink).toHaveClass('selected'); + await completedLink.click(); + + // Page change - completed items. + await expect(completedLink).toHaveClass('selected'); + }); +}); + +async function createDefaultTodos(page: Page) { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + for (const item of TODO_ITEMS) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } +} + +async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) { + return await page.waitForFunction(e => { + return JSON.parse(localStorage['react-todos']).length === e; + }, expected); +} + +async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) { + return await page.waitForFunction(e => { + return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e; + }, expected); +} + +async function checkTodosInLocalStorage(page: Page, title: string) { + return await page.waitForFunction(t => { + return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t); + }, title); +} diff --git a/yarn.lock b/yarn.lock index 5fd6fd06f21..4f53486c587 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1774,6 +1774,16 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@playwright/test@^1.36.2": + version "1.36.2" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.36.2.tgz#9edd68a02b0929c5d78d9479a654ceb981dfb592" + integrity sha512-2rVZeyPRjxfPH6J0oGJqE8YxiM1IBRyM8hyrXYK7eSiAqmbNhxwcLa7dZ7fy9Kj26V7FYia5fh9XJRq4Dqme+g== + dependencies: + "@types/node" "*" + playwright-core "1.36.2" + optionalDependencies: + fsevents "2.3.2" + "@polka/url@^1.0.0-next.20": version "1.0.0-next.21" resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1" @@ -5129,7 +5139,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@^2.3.2, fsevents@~2.3.2: +fsevents@2.3.2, fsevents@^2.3.2, fsevents@~2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== @@ -8256,6 +8266,11 @@ pkg-dir@^4.1.0, pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +playwright-core@1.36.2: + version "1.36.2" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.36.2.tgz#32382f2d96764c24c65a86ea336cf79721c2e50e" + integrity sha512-sQYZt31dwkqxOrP7xy2ggDfEzUxM1lodjhsQ3NMMv5uGTRDsLxU0e4xf4wwMkF2gplIxf17QMBCodSFgm6bFVQ== + plist@^3.0.2: version "3.0.5" resolved "https://registry.yarnpkg.com/plist/-/plist-3.0.5.tgz#2cbeb52d10e3cdccccf0c11a63a85d830970a987"