Skip to content

Commit 9e5fa48

Browse files
authored
test(Playwright): enable Playwright component testing (#8246)
This PR enables Playwright component testing, including coverage. It adds: - Fixtures for custom wc commands (internal and public) - Fixtures for code coverage - SelectDialog test which replaces the unstable Cypress test
1 parent e3f5b92 commit 9e5fa48

20 files changed

Lines changed: 1968 additions & 34 deletions

.github/workflows/test.yml

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,50 @@ on:
99
required: false
1010

1111
jobs:
12+
playwright:
13+
name: Playwright
14+
runs-on: ubuntu-latest
15+
strategy:
16+
matrix:
17+
react: ['18', '19']
18+
fail-fast: false
19+
steps:
20+
- name: Checkout
21+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
22+
23+
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
24+
with:
25+
node-version-file: '.nvmrc'
26+
cache: 'yarn'
27+
28+
- name: Install
29+
run: yarn install --immutable
30+
31+
- name: Install React 18
32+
if: ${{ matrix.react == '18' }}
33+
run: |
34+
yarn add "@types/react@18" "@types/react-dom@18" --dev
35+
yarn add react@18 react-dom@18
36+
37+
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
38+
with:
39+
name: build-${{ matrix.react }}
40+
path: packages
41+
42+
- name: Install Playwright browsers
43+
run: npx playwright install --with-deps chromium
44+
45+
- name: Run Playwright tests
46+
run: yarn test:pw:ci
47+
48+
- uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b # v2.3.6
49+
if: ${{ inputs.reportCoverage && matrix.react == '19' }}
50+
with:
51+
file: temp/playwright-coverage/lcov.info
52+
github-token: ${{ secrets.GITHUB_TOKEN }}
53+
parallel: true
54+
flag-name: playwright
55+
1256
cypress:
1357
name: Cypress
1458
runs-on: ubuntu-latest
@@ -65,7 +109,7 @@ jobs:
65109

66110
coveralls:
67111
if: ${{ always() && inputs.reportCoverage }}
68-
needs: ['cypress']
112+
needs: ['cypress', 'playwright']
69113
name: 'Combine Coverage'
70114
runs-on: ubuntu-latest
71115
steps:
@@ -74,4 +118,4 @@ jobs:
74118
with:
75119
github-token: ${{ secrets.GITHUB_TOKEN }}
76120
parallel-finished: true
77-
carryforward: 'base,charts,cypress-commands,main/src/components,main/src/internal'
121+
carryforward: 'base,charts,cypress-commands,main/src/components,main/src/internal,playwright'

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,10 @@ debug-storybook.log
3535
.vscode
3636
.cursor/rules/nx-rules.mdc
3737
.github/instructions/nx.instructions.md
38+
39+
# Playwright
40+
/test-results/
41+
/playwright-report/
42+
/blob-report/
43+
/playwright/.cache/
44+
/playwright/.auth/

cypress.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export default defineConfig({
2929
bundler: 'vite',
3030
},
3131
experimentalRunAllSpecs: true,
32-
excludeSpecPattern: ['**/e2e/**'],
32+
excludeSpecPattern: ['**/e2e/**', ...(process.env.CI ? ['**/SelectDialog/**'] : [])],
3333
},
3434
includeShadowDom: true,
3535
viewportWidth: 1920,

eslint.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ const config = tseslint.config(
206206
},
207207
},
208208
{
209-
files: ['**/*.cy.ts', '**/*.cy.tsx'],
209+
files: ['**/*.cy.ts', '**/*.cy.tsx', '**/*.spec.ts', '**/*.spec.tsx', 'playwright/**/*'],
210210

211211
plugins: {
212212
'no-only-tests': noOnlyTests,

package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616
"test:prepare": "rimraf temp && lerna run build",
1717
"test:open": "CYPRESS_COVERAGE=false cypress open --component --browser chrome",
1818
"test": "yarn test:prepare && cypress run --component --browser chrome --spec packages",
19-
"clean": "tsc --build --clean && tsc --build tsconfig.build.json --clean && rimraf temp .out && lerna run clean",
19+
"test:pw": "playwright test -c playwright-ct.config.ts",
20+
"test:pw:open": "playwright test -c playwright-ct.config.ts --ui",
21+
"test:pw:ci": "playwright test -c playwright-ct.config.ts playwright/test/ packages/main/src/components/SelectDialog/test/ --project chromium",
22+
"clean": "tsc --build --clean && tsc --build tsconfig.build.json --clean && rimraf temp .out test-results playwright-report playwright-ct && lerna run clean",
2023
"clean:remove-modules": "yarn clean && rimraf node_modules",
2124
"prettier:all": "prettier --write --config ./prettier.config.js \"**/*\"",
2225
"lint": "eslint .",
@@ -60,6 +63,8 @@
6063
"@cypress/code-coverage": "4.0.1",
6164
"@eslint/compat": "2.0.2",
6265
"@eslint/js": "9.39.3",
66+
"@playwright/experimental-ct-react": "1.58.2",
67+
"@playwright/test": "1.58.2",
6368
"@semantic-release/github": "12.0.6",
6469
"@testing-library/cypress": "10.1.0",
6570
"@types/jscodeshift": "17.3.0",
@@ -88,6 +93,7 @@
8893
"husky": "9.1.7",
8994
"lerna": "9.0.5",
9095
"lint-staged": "16.2.7",
96+
"monocart-reporter": "2.10.0",
9197
"npm-run-all2": "8.0.4",
9298
"postcss": "8.5.6",
9399
"postcss-cli": "11.0.1",
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import { expect } from '@playwright/experimental-ct-react';
2+
import { test } from '../../../../../../playwright/fixtures/main-fixtures.js';
3+
import {
4+
SelectDialogBasicTestComp,
5+
SelectDialogHeaderTestComp,
6+
SelectDialogSelectionWithToggleTestComp,
7+
SelectDialogSearchTestComp,
8+
SelectDialogConfirmButtonTextTestComp,
9+
SelectDialogNumberOfSelectedItemsTestComp,
10+
SelectDialogCancelWithToggleTestComp,
11+
SelectDialogConfirmButtonPropsTestComp,
12+
} from './SelectDialogTestComponents.js';
13+
14+
test.describe('SelectDialog', () => {
15+
test('Basic', async ({ mount, page }) => {
16+
await mount(<SelectDialogBasicTestComp />);
17+
await expect(page.locator('[ui5-dialog]')).toBeVisible();
18+
await expect(page.locator('[ui5-input][placeholder="Search"]')).toBeVisible();
19+
await page.getByText('Cancel').click();
20+
await expect(page.locator('[ui5-dialog]')).not.toBeVisible();
21+
});
22+
23+
test('with headerText', async ({ mount, page, ui5wc }) => {
24+
await mount(<SelectDialogHeaderTestComp />);
25+
const header = page.getByText('Select Dialog');
26+
await expect(header).toHaveCSS('grid-column-start', 'titleStart');
27+
await expect(header).toHaveCSS('grid-column-end', 'titleCenter');
28+
await expect(header).toHaveAttribute('level', 'H1');
29+
30+
await ui5wc.closePopupWithEsc();
31+
await page.getByTestId('toggle-center').click();
32+
await page.getByTestId('open-btn').click();
33+
await expect(header).toHaveCSS('grid-area', 'titleCenter');
34+
await expect(header).toHaveAttribute('level', 'H1');
35+
36+
await ui5wc.closePopupWithEsc();
37+
await page.getByTestId('set-h2').click();
38+
await page.getByTestId('open-btn').click();
39+
await expect(header).toHaveAttribute('level', 'H2');
40+
});
41+
42+
test('selection', async ({ mount, page, ui5wc }) => {
43+
await mount(<SelectDialogSelectionWithToggleTestComp />);
44+
const list = page.locator('[ui5-list]');
45+
46+
// Single mode - no rememberSelections
47+
await expect(page.locator('[ui5-dialog]')).toBeVisible();
48+
await list.getByText('Product1').click();
49+
await expect(page.locator('[ui5-dialog]')).not.toBeVisible();
50+
await expect(page.getByTestId('selected-items')).toHaveText('Last Selected Item: Product1');
51+
await page.getByTestId('open-btn').click();
52+
const listItems = page.locator('[ui5-li]');
53+
for (let i = 0; i < 5; i++) {
54+
await expect(listItems.nth(i)).not.toHaveAttribute('selected');
55+
}
56+
await ui5wc.closePopupWithEsc();
57+
58+
// Single mode - with rememberSelections
59+
await page.getByTestId('toggle-remember').click();
60+
await page.getByTestId('open-btn').click();
61+
await list.getByText('Product1').click();
62+
await expect(page.locator('[ui5-dialog]')).not.toBeVisible();
63+
await expect(page.getByTestId('selected-items')).toHaveText('Last Selected Item: Product1');
64+
await page.getByTestId('open-btn').click();
65+
await expect(list.locator('[ui5-li][text="Product1"]')).toHaveAttribute('selected');
66+
for (const text of ['Product0', 'Product2', 'Product3', 'Product4']) {
67+
await expect(list.locator(`[ui5-li][text="${text}"]`)).not.toHaveAttribute('selected');
68+
}
69+
await ui5wc.closePopupWithEsc();
70+
71+
await expect(page.getByTestId('close-count')).toHaveText('4');
72+
await expect(page.getByTestId('confirm-count')).toHaveText('2');
73+
await expect(page.getByTestId('change-count')).toHaveText('2');
74+
75+
// Close via Cancel and Escape
76+
await page.getByTestId('reset').click();
77+
await page.getByTestId('open-btn').click();
78+
await page.getByText('Cancel').click();
79+
await expect(page.locator('[ui5-dialog]')).not.toBeVisible();
80+
await expect(page.getByTestId('close-count')).toHaveText('5');
81+
82+
await page.getByTestId('open-btn').click();
83+
await expect(page.locator('[ui5-dialog]')).toBeVisible();
84+
await ui5wc.closePopupWithEsc();
85+
86+
await expect(page.getByTestId('close-count')).toHaveText('6');
87+
await expect(page.getByTestId('confirm-count')).toHaveText('2');
88+
await expect(page.getByTestId('change-count')).toHaveText('2');
89+
90+
// Multiple mode - no rememberSelections
91+
await page.getByTestId('reset').click();
92+
await page.getByTestId('set-multiple').click();
93+
await page.getByTestId('open-btn').click();
94+
await expect(page.locator('[ui5-dialog]')).toBeVisible();
95+
await list.getByText('Product1').click();
96+
await list.getByText('Product3').click();
97+
await page.getByRole('button', { name: 'Select' }).click();
98+
await expect(page.getByTestId('selected-items')).toHaveText('Last Selected Item: Product1Product3');
99+
100+
await page.getByTestId('open-btn').click();
101+
for (let i = 0; i < 5; i++) {
102+
await expect(listItems.nth(i)).not.toHaveAttribute('selected');
103+
}
104+
105+
// Multiple mode - with rememberSelections
106+
await page.getByText('Cancel').click();
107+
await page.getByTestId('toggle-remember').click();
108+
await page.getByTestId('open-btn').click();
109+
await list.getByText('Product1').click();
110+
await list.getByText('Product3').click();
111+
await expect(page.locator('[ui5-dialog]')).toBeVisible();
112+
await page.getByRole('button', { name: 'Select' }).click();
113+
await expect(page.getByTestId('selected-items')).toHaveText('Last Selected Item: Product1Product3');
114+
await page.getByTestId('open-btn').click();
115+
await expect(list.locator('[ui5-li][text="Product1"]')).toHaveAttribute('selected');
116+
await expect(list.locator('[ui5-li][text="Product3"]')).toHaveAttribute('selected');
117+
for (const text of ['Product0', 'Product2', 'Product4']) {
118+
await expect(list.locator(`[ui5-li][text="${text}"]`)).not.toHaveAttribute('selected');
119+
}
120+
await ui5wc.closePopupWithEsc();
121+
122+
await expect(page.getByTestId('close-count')).toHaveText('10');
123+
await expect(page.getByTestId('confirm-count')).toHaveText('4');
124+
await expect(page.getByTestId('change-count')).toHaveText('6');
125+
});
126+
127+
test('Search', async ({ mount, page, ui5wc }) => {
128+
await mount(<SelectDialogSearchTestComp />);
129+
await expect(page.locator('[accessible-name="Reset"][ui5-icon]')).not.toBeVisible();
130+
131+
const input = page.locator('[ui5-input]');
132+
await ui5wc.typeIntoInput(input, 'Test');
133+
await expect(page.getByTestId('input-val')).toHaveText('input: Test');
134+
await expect(page.getByTestId('search-count')).toHaveText('0');
135+
await expect(page.getByTestId('input-count')).toHaveText('1');
136+
await expect(page.getByTestId('reset-count')).toHaveText('0');
137+
138+
await input.locator('input').press('Enter');
139+
await expect(page.getByTestId('search-val')).toHaveText('search: Test');
140+
await expect(page.getByTestId('search-count')).toHaveText('1');
141+
await expect(page.getByTestId('input-count')).toHaveText('1');
142+
await expect(page.getByTestId('reset-count')).toHaveText('0');
143+
144+
await page.locator('[accessible-name="Search"][ui5-icon]').click();
145+
await expect(page.getByTestId('search-count')).toHaveText('2');
146+
await expect(page.getByTestId('input-count')).toHaveText('1');
147+
await expect(page.getByTestId('reset-count')).toHaveText('0');
148+
149+
await page.locator('[part="clear-icon"][ui5-icon]').click();
150+
await expect(page.getByTestId('search-count')).toHaveText('2');
151+
// clearing the input via clear button fires input event as well
152+
await expect(page.getByTestId('input-count')).toHaveText('2');
153+
await expect(page.getByTestId('reset-count')).toHaveText('1');
154+
await expect(page.locator('[accessible-name="Reset"][ui5-icon]')).not.toBeVisible();
155+
});
156+
157+
test('confirmButtonText', async ({ mount, page }) => {
158+
await mount(<SelectDialogConfirmButtonTextTestComp />);
159+
await expect(page.locator('[ui5-dialog]')).toBeVisible();
160+
await page.getByText('Exterminate').click();
161+
await expect(page.getByTestId('confirm-count')).toHaveText('1');
162+
await expect(page.locator('[ui5-dialog]')).not.toBeVisible();
163+
});
164+
165+
test('numberOfSelectedItems', async ({ mount, page }) => {
166+
await mount(<SelectDialogNumberOfSelectedItemsTestComp />);
167+
await expect(page.getByText('Selected: 1337')).toBeVisible();
168+
});
169+
170+
test('onCancel', async ({ mount, page, ui5wc }) => {
171+
await mount(<SelectDialogCancelWithToggleTestComp />);
172+
173+
// Single mode
174+
await page.getByTestId('open-btn').click();
175+
await page.getByText('Cancel').click();
176+
await expect(page.getByTestId('cancel-count')).toHaveText('1');
177+
178+
await page.getByTestId('open-btn').click();
179+
await expect(page.locator('[ui5-dialog]')).toBeVisible();
180+
await ui5wc.closePopupWithEsc();
181+
await expect(page.getByTestId('cancel-count')).toHaveText('2');
182+
183+
// Multiple mode
184+
await page.getByTestId('set-multiple').click();
185+
await page.getByTestId('open-btn').click();
186+
await page.getByText('Cancel').click();
187+
await expect(page.getByTestId('cancel-count')).toHaveText('3');
188+
189+
await page.getByTestId('open-btn').click();
190+
await expect(page.locator('[ui5-dialog]')).toBeVisible();
191+
await ui5wc.closePopupWithEsc();
192+
await expect(page.getByTestId('cancel-count')).toHaveText('4');
193+
});
194+
195+
test('confirmButtonProps', async ({ mount, page }) => {
196+
await mount(<SelectDialogConfirmButtonPropsTestComp />);
197+
const btn = page.getByTestId('confirmBtn');
198+
await expect(btn).toBeVisible();
199+
await expect(btn).toHaveAttribute('disabled');
200+
await expect(btn).toHaveAttribute('design', 'Emphasized');
201+
});
202+
});

0 commit comments

Comments
 (0)