From b4fe550141b775728269ed4515278f39915fe1a5 Mon Sep 17 00:00:00 2001 From: Artem Belik Date: Thu, 2 Jul 2026 12:08:33 +0300 Subject: [PATCH] chore: added eslint-plugin-playwright --- .eslintignore | 2 ++ .eslintrc.js | 13 +++++++++ .../src/tests/column-menu.playwright-spec.ts | 10 ++++--- .../src/tests/column-state.playwright-spec.ts | 28 +++++++++---------- .../external-filter-state.playwright-spec.ts | 4 +-- .../infinite-selection.playwright-spec.ts | 1 + .../tests/loading-overlay.playwright-spec.ts | 2 +- .../quick-filter-state.playwright-spec.ts | 4 +-- .../src/tests/theme.playwright-spec.ts | 6 ++-- package.json | 1 + yarn.lock | 19 +++++++++++++ 11 files changed, 64 insertions(+), 26 deletions(-) diff --git a/.eslintignore b/.eslintignore index 235c6e1..fe4c9a6 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,6 +1,8 @@ dist node_modules tmp +.angular +playwright-report # eslint ignores root level "dot" files by default !/.*.js diff --git a/.eslintrc.js b/.eslintrc.js index 640625b..8c95ae3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -171,6 +171,18 @@ const ngTemplateRules = { } }; +/** @type {import('eslint').Linter.ConfigOverride} */ +const playwrightRules = { + files: ['*.playwright-spec.ts'], + extends: ['plugin:playwright/recommended'], + rules: { + 'playwright/expect-expect': [ + 'warn', + { assertFunctionNames: ['expect', 'expectFilterValue', 'waitForRowSelected'] } + ] + } +}; + /** @type {import('eslint').Linter.ConfigOverride} */ const prettierRules = { files: ['*.js', '*.ts', '*.html'], @@ -199,6 +211,7 @@ const config = { tsPackagesRules, ngTsPackagesRules, ngTemplateRules, + playwrightRules, // should be last prettierRules ] diff --git a/dev/ag-grid-angular/src/tests/column-menu.playwright-spec.ts b/dev/ag-grid-angular/src/tests/column-menu.playwright-spec.ts index 2c8dad6..c5860b4 100644 --- a/dev/ag-grid-angular/src/tests/column-menu.playwright-spec.ts +++ b/dev/ag-grid-angular/src/tests/column-menu.playwright-spec.ts @@ -34,8 +34,10 @@ const drag = async (page: Page, source: Locator, target: Locator): Promise await page.mouse.move(srcX, srcY); await page.mouse.down(); await page.mouse.move(srcX, srcY - 10); // past CDK's 5-px drag-start threshold + // eslint-disable-next-line playwright/no-wait-for-timeout await page.waitForTimeout(100); await page.mouse.move(tgtX, tgtY, { steps: 50 }); // slow enough for CDK to register each position + // eslint-disable-next-line playwright/no-wait-for-timeout await page.waitForTimeout(150); // hold so CDK registers the active drop list await page.mouse.up(); }; @@ -72,7 +74,7 @@ test.describe('KbqAgGridColumnMenu', () => { await getRow(getSection(page, 'Visible'), 'Country').locator('.kbq-column-menu-checkbox').click(); - await expect(page.locator('.ag-header-cell[col-id="country"]')).not.toBeVisible(); + await expect(page.locator('.ag-header-cell[col-id="country"]')).toBeHidden(); }); test('showing a hidden column via panel removes it from the Hidden section', async ({ page }) => { @@ -126,7 +128,7 @@ test.describe('KbqAgGridColumnMenu', () => { await getRow(getSection(page, 'Pinned Right'), 'Date').locator(`[title="${LABEL_UNPIN}"]`).click(); - await expect(page.locator('.ag-pinned-right-header .ag-header-cell[col-id="date"]')).not.toBeVisible(); + await expect(page.locator('.ag-pinned-right-header .ag-header-cell[col-id="date"]')).toBeHidden(); await expect .poll(async () => { const state = await api.evaluate((gridApi) => gridApi.getColumnState().find((s) => s.colId === 'date')); @@ -152,7 +154,7 @@ test.describe('KbqAgGridColumnMenu', () => { await page.locator('.kbq-column-menu-reset-btn').click(); await expect(page.locator('.ag-header-cell[col-id="age"]')).toBeVisible(); - await expect(page.locator('.ag-pinned-left-header .ag-header-cell[col-id="athlete"]')).not.toBeVisible(); + await expect(page.locator('.ag-pinned-left-header .ag-header-cell[col-id="athlete"]')).toBeHidden(); await expect(page.locator('.ag-pinned-right-header .ag-header-cell[col-id="date"]')).toBeVisible(); }); @@ -206,6 +208,6 @@ test.describe('KbqAgGridColumnMenu', () => { }) .toBeFalsy(); - await expect(page.locator('.kbq-column-menu-section-label', { hasText: 'Pinned Left' })).not.toBeVisible(); + await expect(page.locator('.kbq-column-menu-section-label', { hasText: 'Pinned Left' })).toBeHidden(); }); }); diff --git a/dev/ag-grid-angular/src/tests/column-state.playwright-spec.ts b/dev/ag-grid-angular/src/tests/column-state.playwright-spec.ts index c5ee4f1..9f42184 100644 --- a/dev/ag-grid-angular/src/tests/column-state.playwright-spec.ts +++ b/dev/ag-grid-angular/src/tests/column-state.playwright-spec.ts @@ -77,11 +77,11 @@ test.describe('KbqAgGridColumnState', () => { const initialWidth = await headerCell.evaluate((el: Element) => el.getBoundingClientRect().width); const bounds = await resizer.boundingBox(); - if (!bounds) throw new Error('resizer not found'); + expect(bounds).not.toBeNull(); - await page.mouse.move(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2); + await page.mouse.move(bounds!.x + bounds!.width / 2, bounds!.y + bounds!.height / 2); await page.mouse.down(); - await page.mouse.move(bounds.x + bounds.width / 2 + 60, bounds.y); + await page.mouse.move(bounds!.x + bounds!.width / 2 + 60, bounds!.y); await page.mouse.up(); await expect @@ -99,11 +99,11 @@ test.describe('KbqAgGridColumnState', () => { const resizer = page.locator('.ag-header-cell[col-id="athlete"] .ag-header-cell-resize'); const bounds = await resizer.boundingBox(); - if (!bounds) throw new Error('resizer not found'); + expect(bounds).not.toBeNull(); - await page.mouse.move(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2); + await page.mouse.move(bounds!.x + bounds!.width / 2, bounds!.y + bounds!.height / 2); await page.mouse.down(); - await page.mouse.move(bounds.x + bounds.width / 2 + 60, bounds.y); + await page.mouse.move(bounds!.x + bounds!.width / 2 + 60, bounds!.y); await page.mouse.up(); const widthBeforeReload = await page @@ -213,7 +213,7 @@ test.describe('KbqAgGridColumnState', () => { ); await page.goto('/e2e/column-state'); - await expect(page.locator('.ag-header-cell[col-id="athlete"]')).not.toBeVisible(); + await expect(page.locator('.ag-header-cell[col-id="athlete"]')).toBeHidden(); }); }); @@ -255,11 +255,11 @@ test.describe('KbqAgGridColumnState', () => { const initialWidth = await headerCell.evaluate((el: Element) => el.getBoundingClientRect().width); const bounds = await resizer.boundingBox(); - if (!bounds) throw new Error('resizer not found'); + expect(bounds).not.toBeNull(); - await page.mouse.move(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2); + await page.mouse.move(bounds!.x + bounds!.width / 2, bounds!.y + bounds!.height / 2); await page.mouse.down(); - await page.mouse.move(bounds.x + bounds.width / 2 + 60, bounds.y); + await page.mouse.move(bounds!.x + bounds!.width / 2 + 60, bounds!.y); await page.mouse.up(); await expect @@ -276,11 +276,11 @@ test.describe('KbqAgGridColumnState', () => { const resizer = page.locator('.ag-header-cell[col-id="athlete"] .ag-header-cell-resize'); const bounds = await resizer.boundingBox(); - if (!bounds) throw new Error('resizer not found'); + expect(bounds).not.toBeNull(); - await page.mouse.move(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2); + await page.mouse.move(bounds!.x + bounds!.width / 2, bounds!.y + bounds!.height / 2); await page.mouse.down(); - await page.mouse.move(bounds.x + bounds.width / 2 + 60, bounds.y); + await page.mouse.move(bounds!.x + bounds!.width / 2 + 60, bounds!.y); await page.mouse.up(); const widthBeforeReload = await page @@ -380,7 +380,7 @@ test.describe('KbqAgGridColumnState', () => { ]) ); - await expect(page.locator('.ag-header-cell[col-id="athlete"]')).not.toBeVisible(); + await expect(page.locator('.ag-header-cell[col-id="athlete"]')).toBeHidden(); }); }); }); diff --git a/dev/ag-grid-angular/src/tests/external-filter-state.playwright-spec.ts b/dev/ag-grid-angular/src/tests/external-filter-state.playwright-spec.ts index da4e3e6..b24f0ed 100644 --- a/dev/ag-grid-angular/src/tests/external-filter-state.playwright-spec.ts +++ b/dev/ag-grid-angular/src/tests/external-filter-state.playwright-spec.ts @@ -72,7 +72,7 @@ test.describe('KbqAgGridExternalFilterState', () => { await page.goto('/e2e/external-filter-state'); await expect.poll(async () => getDisplayedRowCount(page), { timeout: 10_000 }).toBeGreaterThan(0); - await expect(page.locator('.ag-overlay-no-rows-center')).not.toBeVisible(); + await expect(page.locator('.ag-overlay-no-rows-center')).toBeHidden(); }); test('removes external filter state from localStorage when filter is cleared', async ({ page }) => { @@ -120,7 +120,7 @@ test.describe('KbqAgGridExternalFilterState', () => { await page.goto(buildExternalFilterUrl(OPTION)); await expect.poll(async () => getDisplayedRowCount(page), { timeout: 10_000 }).toBeGreaterThan(0); - await expect(page.locator('.ag-overlay-no-rows-center')).not.toBeVisible(); + await expect(page.locator('.ag-overlay-no-rows-center')).toBeHidden(); }); test('removes external filter state from URL when filter is cleared', async ({ page }) => { diff --git a/dev/ag-grid-angular/src/tests/infinite-selection.playwright-spec.ts b/dev/ag-grid-angular/src/tests/infinite-selection.playwright-spec.ts index cbf2626..7548dd7 100644 --- a/dev/ag-grid-angular/src/tests/infinite-selection.playwright-spec.ts +++ b/dev/ag-grid-angular/src/tests/infinite-selection.playwright-spec.ts @@ -203,6 +203,7 @@ test.describe('KbqAgGridInfiniteSelection', () => { await waitForRowSelected(page, 0); await page.keyboard.press('Control+a'); // already all selected — should stay selected + // eslint-disable-next-line playwright/no-wait-for-timeout await page.waitForTimeout(200); // give time for any (unexpected) state change const state = await getState(page); diff --git a/dev/ag-grid-angular/src/tests/loading-overlay.playwright-spec.ts b/dev/ag-grid-angular/src/tests/loading-overlay.playwright-spec.ts index 9c952a0..d2a3dad 100644 --- a/dev/ag-grid-angular/src/tests/loading-overlay.playwright-spec.ts +++ b/dev/ag-grid-angular/src/tests/loading-overlay.playwright-spec.ts @@ -26,7 +26,7 @@ test.describe('KbqAgGridLoadingOverlay', () => { test('hides overlay when toggle button is clicked', async ({ page }) => { await page.goto('/e2e/loading-overlay'); await getToggleButton(page).click(); - await expect(getOverlay(page)).not.toBeVisible(); + await expect(getOverlay(page)).toBeHidden(); }); test('shows overlay again after second toggle', async ({ page }) => { diff --git a/dev/ag-grid-angular/src/tests/quick-filter-state.playwright-spec.ts b/dev/ag-grid-angular/src/tests/quick-filter-state.playwright-spec.ts index e76077b..45d2b82 100644 --- a/dev/ag-grid-angular/src/tests/quick-filter-state.playwright-spec.ts +++ b/dev/ag-grid-angular/src/tests/quick-filter-state.playwright-spec.ts @@ -67,7 +67,7 @@ test.describe('KbqAgGridQuickFilterState', () => { await expect.poll(async () => getDisplayedRowCount(page), { timeout: 10_000 }).toBeGreaterThan(0); await expect(page.locator('.ag-row')).not.toHaveCount(0); - await expect(page.locator('.ag-overlay-no-rows-center')).not.toBeVisible(); + await expect(page.locator('.ag-overlay-no-rows-center')).toBeHidden(); }); test('removes quick filter state from localStorage when text is cleared', async ({ page }) => { @@ -123,7 +123,7 @@ test.describe('KbqAgGridQuickFilterState', () => { await expect.poll(async () => getDisplayedRowCount(page), { timeout: 10_000 }).toBeGreaterThan(0); await expect(page.locator('.ag-row')).not.toHaveCount(0); - await expect(page.locator('.ag-overlay-no-rows-center')).not.toBeVisible(); + await expect(page.locator('.ag-overlay-no-rows-center')).toBeHidden(); }); test('removes quick filter state from URL when text is cleared', async ({ page }) => { diff --git a/dev/ag-grid-angular/src/tests/theme.playwright-spec.ts b/dev/ag-grid-angular/src/tests/theme.playwright-spec.ts index dd4e92a..42d4c96 100644 --- a/dev/ag-grid-angular/src/tests/theme.playwright-spec.ts +++ b/dev/ag-grid-angular/src/tests/theme.playwright-spec.ts @@ -62,9 +62,9 @@ test.describe('KbqAgGridAngularTheme', () => { test('with opened filter popup', async ({ page }) => { await page.setViewportSize({ width: 768, height: 500 }); await page.goto('/e2e/theme'); - await page.hover('.ag-header-cell[col-id="athlete"]'); - await page.click('.ag-header-cell[col-id="athlete"] .ag-header-cell-filter-button'); - await page.click('.ag-menu .ag-filter-select'); + await page.locator('.ag-header-cell[col-id="athlete"]').hover(); + await page.locator('.ag-header-cell[col-id="athlete"] .ag-header-cell-filter-button').click(); + await page.locator('.ag-menu .ag-filter-select').click(); await expect(getScreenshotTarget(page)).toHaveScreenshot('theme-filter-popup-light.png'); await enableDarkTheme(page); await expect(getScreenshotTarget(page)).toHaveScreenshot('theme-filter-popup-dark.png'); diff --git a/package.json b/package.json index adc68e0..ebf6572 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "eslint": "^8.57.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-playwright": "^2.10.4", "eslint-plugin-prettier": "^5.5.6", "eslint-plugin-promise": "^7.3.0", "eslint-plugin-rxjs": "^5.0.3", diff --git a/yarn.lock b/yarn.lock index 824b31f..72e4573 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10415,6 +10415,7 @@ __metadata: eslint: "npm:^8.57.0" eslint-config-prettier: "npm:^10.1.8" eslint-plugin-eslint-comments: "npm:^3.2.0" + eslint-plugin-playwright: "npm:^2.10.4" eslint-plugin-prettier: "npm:^5.5.6" eslint-plugin-promise: "npm:^7.3.0" eslint-plugin-rxjs: "npm:^5.0.3" @@ -11645,6 +11646,17 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-playwright@npm:^2.10.4": + version: 2.10.4 + resolution: "eslint-plugin-playwright@npm:2.10.4" + dependencies: + globals: "npm:^17.3.0" + peerDependencies: + eslint: ">=8.40.0" + checksum: 10c0/515aac2a870b217f23637bfdab26b9125e47cff91625dbafe20617b3522430fdee8961780f6b7dc9fcfcdbec8235ccd30eafd0ac871088cf3af4d70b8f9b7e29 + languageName: node + linkType: hard + "eslint-plugin-prettier@npm:^5.5.6": version: 5.5.6 resolution: "eslint-plugin-prettier@npm:5.5.6" @@ -12990,6 +13002,13 @@ __metadata: languageName: node linkType: hard +"globals@npm:^17.3.0": + version: 17.7.0 + resolution: "globals@npm:17.7.0" + checksum: 10c0/e83f791acf537b85fa1632ac48a45432a1999a61acd9f955b5c2145f66d6b5000a7945029874b6184fa2dfac8d33af03885a5a7ffa457866f1bcf0b40d0c1291 + languageName: node + linkType: hard + "globby@npm:^11.1.0": version: 11.1.0 resolution: "globby@npm:11.1.0"