From 5671270d26eb3fa66e6fb7a319c45dcdd7c82ad0 Mon Sep 17 00:00:00 2001 From: Andrei Fateev Date: Tue, 11 Aug 2026 13:50:18 +0200 Subject: [PATCH 1/2] test: add Playwright coverage and test ids for CpsTooltipDirective --- .../cps-ui-kit/directives/cps-tooltip.spec.ts | 171 ++++++++++++++++++ .../tooltip-page/tooltip-page.component.html | 35 ++++ .../tooltip-page/tooltip-page.examples.ts | 21 +++ .../cps-tooltip/cps-tooltip.directive.spec.ts | 4 +- .../cps-tooltip/cps-tooltip.directive.ts | 2 + 5 files changed, 231 insertions(+), 2 deletions(-) create mode 100644 playwright/cps-ui-kit/directives/cps-tooltip.spec.ts diff --git a/playwright/cps-ui-kit/directives/cps-tooltip.spec.ts b/playwright/cps-ui-kit/directives/cps-tooltip.spec.ts new file mode 100644 index 000000000..35914f518 --- /dev/null +++ b/playwright/cps-ui-kit/directives/cps-tooltip.spec.ts @@ -0,0 +1,171 @@ +import { test, expect, type Page, type Locator } from '@playwright/test'; + +function button(page: Page, testId: string): Locator { + return page.getByTestId(testId); +} + +function tooltip(page: Page): Locator { + return page.getByTestId('cps-tooltip'); +} + +test.describe('cps-tooltip', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/tooltip'); + }); + + test.describe('Real click-trigger isolation from hover', () => { + test('hovering does nothing, clicking shows the real tooltip', async ({ + page + }) => { + const target = button(page, 'click-open-tooltip'); + + await target.hover(); + await expect(tooltip(page)).toHaveCount(0); + + await target.click(); + await expect(tooltip(page)).toBeVisible(); + await expect(tooltip(page)).toHaveText('Triggered on click'); + }); + }); + + test.describe('Real focus-only-trigger isolation from hover', () => { + test('hovering the target does not show the tooltip', async ({ page }) => { + const target = button(page, 'focus-only-tooltip'); + + await target.hover(); + await expect(tooltip(page)).toHaveCount(0); + }); + + test('real keyboard focus shows the real tooltip', async ({ page }) => { + const target = button(page, 'focus-only-tooltip'); + const innerButton = target.getByTestId('cps-button'); + + await innerButton.focus(); + const ownTooltip = page.getByRole('tooltip', { + name: 'Triggered on focus only' + }); + await expect(ownTooltip).toBeVisible(); + }); + }); + + test.describe('Real custom open-delay timing', () => { + test('the tooltip is not visible immediately after hover but appears within the configured delay', async ({ + page + }) => { + const target = button(page, 'open-delay-tooltip'); + + await target.hover(); + await expect(tooltip(page)).not.toBeVisible({ timeout: 200 }); + await expect(tooltip(page)).toBeVisible({ timeout: 1500 }); + }); + }); + + test.describe('Real custom close-delay timing', () => { + test('the tooltip stays visible briefly after mouse-leave before disappearing', async ({ + page + }) => { + const target = button(page, 'close-delay-tooltip'); + + await target.hover(); + await expect(tooltip(page)).toBeVisible(); + + await page.mouse.move(0, 0); + await expect(tooltip(page)).toBeVisible({ timeout: 200 }); + await expect(tooltip(page)).toHaveCount(0, { timeout: 1500 }); + }); + }); + + test.describe('tooltipDisabled renders no popup at all', () => { + test('hovering a disabled tooltip target creates no real tooltip node', async ({ + page + }) => { + const target = button(page, 'disabled-state-tooltip'); + + await target.hover(); + await expect(tooltip(page)).toHaveCount(0); + }); + }); + + test.describe('Real tooltipMaxWidth enforcement', () => { + test('the real rendered tooltip width respects the configured max width', async ({ + page + }) => { + const target = button(page, 'custom-offset-tooltip'); + + await target.hover(); + const box = await tooltip(page).boundingBox(); + if (!box) throw new Error('boundingBox() returned null'); + + const maxWidthPx = await page.evaluate( + () => + parseFloat(getComputedStyle(document.documentElement).fontSize) * 8 + ); + expect(box.width).toBeLessThanOrEqual(maxWidthPx + 1); + }); + }); + + test.describe('Real tooltipOffset distance', () => { + test('the real gap between target and tooltip matches the configured offset', async ({ + page + }) => { + const target = button(page, 'custom-offset-tooltip'); + + await target.hover(); + const targetBox = await target.boundingBox(); + const tooltipBox = await tooltip(page).boundingBox(); + if (!targetBox || !tooltipBox) + throw new Error('boundingBox() returned null'); + + const rootFontSizePx = await page.evaluate(() => + parseFloat(getComputedStyle(document.documentElement).fontSize) + ); + const gap = tooltipBox.x - (targetBox.x + targetBox.width); + expect(gap).toBeGreaterThan(rootFontSizePx * 1.5); + expect(gap).toBeLessThan(rootFontSizePx * 2.5); + }); + }); + + test.describe('Real click-trigger aria-live announce', () => { + test('clicking creates a real self-removing aria-live region with the tooltip text', async ({ + page + }) => { + const target = button(page, 'click-open-tooltip'); + + await target.click(); + const announce = page.locator('.cps-sr-only[aria-live="assertive"]'); + await expect(announce).toHaveText('Triggered on click'); + }); + }); + + test.describe('Real destroy on scroll and resize', () => { + test('a real scroll event immediately removes the tooltip with no lingering node', async ({ + page + }) => { + const target = button(page, 'bottom-position-tooltip'); + + await target.hover(); + await expect(tooltip(page)).toBeVisible(); + + await page.mouse.wheel(0, 10); + + await expect(tooltip(page)).toHaveCount(0); + }); + + test('a real window resize immediately removes the tooltip with no lingering node', async ({ + page + }) => { + const target = button(page, 'bottom-position-tooltip'); + const originalSize = page.viewportSize(); + + await target.hover(); + await expect(tooltip(page)).toBeVisible(); + + await page.setViewportSize({ + width: (originalSize?.width ?? 1280) - 10, + height: originalSize?.height ?? 720 + }); + + await expect(tooltip(page)).toHaveCount(0); + }); + }); +}); diff --git a/projects/composition/src/app/pages/tooltip-page/tooltip-page.component.html b/projects/composition/src/app/pages/tooltip-page/tooltip-page.component.html index 8db2d7ebb..3a37826bf 100644 --- a/projects/composition/src/app/pages/tooltip-page/tooltip-page.component.html +++ b/projects/composition/src/app/pages/tooltip-page/tooltip-page.component.html @@ -5,21 +5,25 @@ [htmlCode]="examples.tooltipPositions.html">
@@ -32,12 +36,14 @@ [htmlCode]="examples.openingMethods.html">
+ + + + +
+ + + + + diff --git a/projects/composition/src/app/pages/tooltip-page/tooltip-page.examples.ts b/projects/composition/src/app/pages/tooltip-page/tooltip-page.examples.ts index 1b8999d43..ec64216af 100644 --- a/projects/composition/src/app/pages/tooltip-page/tooltip-page.examples.ts +++ b/projects/composition/src/app/pages/tooltip-page/tooltip-page.examples.ts @@ -39,6 +39,16 @@ export const tooltipExamples: Record = {
` }, + focusOnlyTooltip: { + html: ` + +` + }, + openCloseDelays: { html: ` = { [label]="ttipEnabled ? 'Deactivate' : 'Activate'">`, ts: ` ttipEnabled = false;` + }, + + customOffsetMaxWidthTooltip: { + html: ` + +` } }; diff --git a/projects/cps-ui-kit/src/lib/directives/cps-tooltip/cps-tooltip.directive.spec.ts b/projects/cps-ui-kit/src/lib/directives/cps-tooltip/cps-tooltip.directive.spec.ts index 9b97d1931..eb92076c9 100644 --- a/projects/cps-ui-kit/src/lib/directives/cps-tooltip/cps-tooltip.directive.spec.ts +++ b/projects/cps-ui-kit/src/lib/directives/cps-tooltip/cps-tooltip.directive.spec.ts @@ -78,7 +78,7 @@ describe('CpsTooltipDirective', () => { expect(tooltipElement).toBeTruthy(); expect(tooltipElement?.innerHTML).toBe( - '
Add your text to this tooltip
' + '
Add your text to this tooltip
' ); // Angular informs about stripping some content during sanitization expect(consoleWarnSpy).toHaveBeenCalledWith( @@ -105,7 +105,7 @@ describe('CpsTooltipDirective', () => { expect(tooltipElement).toBeTruthy(); expect(tooltipElement?.innerHTML).toBe( - '

Legit tooltip

' + '

Legit tooltip

' ); divElement.triggerEventHandler('mouseleave', null); diff --git a/projects/cps-ui-kit/src/lib/directives/cps-tooltip/cps-tooltip.directive.ts b/projects/cps-ui-kit/src/lib/directives/cps-tooltip/cps-tooltip.directive.ts index 21c577ab9..ed0314f32 100644 --- a/projects/cps-ui-kit/src/lib/directives/cps-tooltip/cps-tooltip.directive.ts +++ b/projects/cps-ui-kit/src/lib/directives/cps-tooltip/cps-tooltip.directive.ts @@ -269,10 +269,12 @@ export class CpsTooltipDirective implements OnDestroy { this._domSanitizer.sanitize(SecurityContext.HTML, this.tooltip()) || 'Add your text to this tooltip'; popupContent.className = this.tooltipContentClass(); + popupContent.setAttribute('data-testid', 'cps-tooltip-content'); this._popup.appendChild(popupContent); this._popup.classList.add('cps-tooltip'); this._popup.style.maxWidth = convertSize(this.tooltipMaxWidth()); this._popup.setAttribute('role', 'tooltip'); + this._popup.setAttribute('data-testid', 'cps-tooltip'); this._document.body.appendChild(this._popup); this._ariaTarget?.setAttribute( 'aria-description', From 6002cf21d44675e693f7e41294394addeace0a7e Mon Sep 17 00:00:00 2001 From: Andrei Fateev Date: Tue, 11 Aug 2026 14:20:15 +0200 Subject: [PATCH 2/2] address feedback --- .../cps-ui-kit/directives/cps-tooltip.spec.ts | 21 +++++++++++++++++-- .../cps-tooltip/cps-tooltip.directive.spec.ts | 12 +++++++---- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/playwright/cps-ui-kit/directives/cps-tooltip.spec.ts b/playwright/cps-ui-kit/directives/cps-tooltip.spec.ts index 35914f518..3853ff1e6 100644 --- a/playwright/cps-ui-kit/directives/cps-tooltip.spec.ts +++ b/playwright/cps-ui-kit/directives/cps-tooltip.spec.ts @@ -8,6 +8,23 @@ function tooltip(page: Page): Locator { return page.getByTestId('cps-tooltip'); } +function gapBetween( + targetBox: { x: number; y: number; width: number; height: number }, + tooltipBox: { x: number; y: number; width: number; height: number } +): number { + const horizontalOverlap = + tooltipBox.x < targetBox.x + targetBox.width && + tooltipBox.x + tooltipBox.width > targetBox.x; + if (!horizontalOverlap) { + return tooltipBox.x >= targetBox.x + targetBox.width + ? tooltipBox.x - (targetBox.x + targetBox.width) + : targetBox.x - (tooltipBox.x + tooltipBox.width); + } + return tooltipBox.y >= targetBox.y + targetBox.height + ? tooltipBox.y - (targetBox.y + targetBox.height) + : targetBox.y - (tooltipBox.y + tooltipBox.height); +} + test.describe('cps-tooltip', () => { test.beforeEach(async ({ page }) => { await page.goto('/tooltip'); @@ -119,7 +136,7 @@ test.describe('cps-tooltip', () => { const rootFontSizePx = await page.evaluate(() => parseFloat(getComputedStyle(document.documentElement).fontSize) ); - const gap = tooltipBox.x - (targetBox.x + targetBox.width); + const gap = gapBetween(targetBox, tooltipBox); expect(gap).toBeGreaterThan(rootFontSizePx * 1.5); expect(gap).toBeLessThan(rootFontSizePx * 2.5); }); @@ -146,7 +163,7 @@ test.describe('cps-tooltip', () => { await target.hover(); await expect(tooltip(page)).toBeVisible(); - await page.mouse.wheel(0, 10); + await page.evaluate(() => window.dispatchEvent(new Event('scroll'))); await expect(tooltip(page)).toHaveCount(0); }); diff --git a/projects/cps-ui-kit/src/lib/directives/cps-tooltip/cps-tooltip.directive.spec.ts b/projects/cps-ui-kit/src/lib/directives/cps-tooltip/cps-tooltip.directive.spec.ts index eb92076c9..9769f22b0 100644 --- a/projects/cps-ui-kit/src/lib/directives/cps-tooltip/cps-tooltip.directive.spec.ts +++ b/projects/cps-ui-kit/src/lib/directives/cps-tooltip/cps-tooltip.directive.spec.ts @@ -77,9 +77,11 @@ describe('CpsTooltipDirective', () => { document.body.querySelector('.cps-tooltip'); expect(tooltipElement).toBeTruthy(); - expect(tooltipElement?.innerHTML).toBe( - '
Add your text to this tooltip
' + const content = tooltipElement?.querySelector( + '[data-testid="cps-tooltip-content"]' ); + expect(content).toBeTruthy(); + expect(content?.textContent).toBe('Add your text to this tooltip'); // Angular informs about stripping some content during sanitization expect(consoleWarnSpy).toHaveBeenCalledWith( expect.stringContaining('sanitizing HTML stripped some content') @@ -104,9 +106,11 @@ describe('CpsTooltipDirective', () => { document.body.querySelector('.cps-tooltip'); expect(tooltipElement).toBeTruthy(); - expect(tooltipElement?.innerHTML).toBe( - '

Legit tooltip

' + const content = tooltipElement?.querySelector( + '[data-testid="cps-tooltip-content"]' ); + expect(content).toBeTruthy(); + expect(content?.querySelector('h1')?.textContent).toBe('Legit tooltip'); divElement.triggerEventHandler('mouseleave', null); legitComponentFixture.detectChanges();