From 5ea7ba6086d4a4e2b566dc6c4fd4b68da31de66f Mon Sep 17 00:00:00 2001 From: Andrei Fateev Date: Thu, 30 Jul 2026 12:05:19 +0200 Subject: [PATCH 1/2] test: add Playwright coverage and test ids for CpsInfoCircleComponent --- .../components/cps-info-circle.spec.ts | 171 ++++++++++++++++++ .../info-circle-page.component.html | 15 ++ .../info-circle-page.examples.ts | 9 + .../cps-info-circle.component.html | 1 + 4 files changed, 196 insertions(+) create mode 100644 playwright/cps-ui-kit/components/cps-info-circle.spec.ts diff --git a/playwright/cps-ui-kit/components/cps-info-circle.spec.ts b/playwright/cps-ui-kit/components/cps-info-circle.spec.ts new file mode 100644 index 000000000..5e2d3917a --- /dev/null +++ b/playwright/cps-ui-kit/components/cps-info-circle.spec.ts @@ -0,0 +1,171 @@ +import { test, expect, type Page, type Locator } from '@playwright/test'; + +function icon(page: Page, testId: string): Locator { + return page.getByTestId(testId).getByTestId('cps-info-circle'); +} + +async function tabUntilFocused(page: Page, target: Locator): Promise { + await target.scrollIntoViewIfNeeded(); + for (let i = 0; i < 30; i++) { + const isFocused = await target.evaluate( + (el) => el === document.activeElement + ); + if (isFocused) return; + await page.keyboard.press('Tab'); + } + throw new Error('Target was never reached via real Tab navigation'); +} + +test.describe('cps-info-circle', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/info-circle'); + }); + + test.describe('Real hover shows and mouse-leave hides the tooltip', () => { + test('hovering shows the real tooltip with the configured text, and moving away removes it', async ({ + page + }) => { + const target = icon(page, 'xsmall-top-info-circle'); + const tooltip = page.locator('.cps-tooltip'); + + await target.hover(); + await expect(tooltip).toBeVisible(); + await expect(tooltip).toHaveText('Provide any information here'); + await expect(target).not.toHaveAttribute('aria-description'); + + await page.mouse.move(0, 0); + await expect(tooltip).toHaveCount(0); + }); + }); + + test.describe('Real keyboard focus also shows the tooltip', () => { + test('tabbing to the icon shows the real tooltip and sets a real aria-description', async ({ + page + }) => { + const target = icon(page, 'xsmall-top-info-circle'); + const tooltip = page.locator('.cps-tooltip'); + + await tabUntilFocused(page, target); + + await expect(tooltip).toBeVisible(); + await expect(tooltip).toHaveText('Provide any information here'); + await expect(target).toHaveAttribute( + 'aria-description', + 'Provide any information here' + ); + }); + }); + + test.describe('Real tooltip positioning', () => { + test('the tooltip renders on the real configured side of the icon for each position', async ({ + page + }) => { + const tooltip = page.locator('.cps-tooltip'); + + const top = icon(page, 'xsmall-top-info-circle'); + await top.hover(); + await expect(tooltip).toBeVisible(); + const topIconBox = await top.boundingBox(); + const topTooltipBox = await tooltip.boundingBox(); + if (!topIconBox || !topTooltipBox) + throw new Error('boundingBox() returned null'); + expect(topTooltipBox.y + topTooltipBox.height).toBeLessThanOrEqual( + topIconBox.y + ); + await page.mouse.move(0, 0); + await expect(tooltip).toHaveCount(0); + + const right = icon(page, 'small-right-info-circle'); + await right.hover(); + await expect(tooltip).toBeVisible(); + const rightIconBox = await right.boundingBox(); + const rightTooltipBox = await tooltip.boundingBox(); + if (!rightIconBox || !rightTooltipBox) + throw new Error('boundingBox() returned null'); + expect(rightTooltipBox.x).toBeGreaterThanOrEqual( + rightIconBox.x + rightIconBox.width + ); + await page.mouse.move(0, 0); + await expect(tooltip).toHaveCount(0); + + const left = icon(page, 'normal-left-info-circle'); + await left.hover(); + await expect(tooltip).toBeVisible(); + const leftIconBox = await left.boundingBox(); + const leftTooltipBox = await tooltip.boundingBox(); + if (!leftIconBox || !leftTooltipBox) + throw new Error('boundingBox() returned null'); + expect(leftTooltipBox.x + leftTooltipBox.width).toBeLessThanOrEqual( + leftIconBox.x + ); + await page.mouse.move(0, 0); + await expect(tooltip).toHaveCount(0); + + const bottom = icon(page, 'large-bottom-info-circle'); + await bottom.hover(); + await expect(tooltip).toBeVisible(); + const bottomIconBox = await bottom.boundingBox(); + const bottomTooltipBox = await tooltip.boundingBox(); + if (!bottomIconBox || !bottomTooltipBox) + throw new Error('boundingBox() returned null'); + expect(bottomTooltipBox.y).toBeGreaterThanOrEqual( + bottomIconBox.y + bottomIconBox.height + ); + }); + }); + + test.describe('Real accessible-name computation', () => { + test('the icon exposes its real accessible name from ariaLabel', async ({ + page + }) => { + await expect( + page + .getByTestId('xsmall-top-info-circle') + .getByRole('img', { name: 'Information' }) + ).toBeVisible(); + }); + }); + + test.describe('Real persistent tooltip', () => { + test('stays open without hover, a click inside does not close it, and a click outside does', async ({ + page + }) => { + const target = icon(page, 'persistent-info-circle'); + const tooltip = page.getByRole('tooltip', { + name: /This tooltip stays open/ + }); + + await target.hover(); + await expect(tooltip).toBeVisible(); + + await page.mouse.move(0, 0); + await expect(tooltip).toBeVisible(); + + const box = await tooltip.boundingBox(); + if (!box) throw new Error('boundingBox() returned null'); + await page.mouse.click(box.x + 5, box.y + 5); + await expect(tooltip).toBeVisible(); + + await page.mouse.click(10, 10); + await expect(tooltip).toHaveCount(0); + }); + }); + + test.describe('Real Tab-trapping into persistent tooltip content', () => { + test('tabbing from the icon moves focus into the tooltip real focusable content', async ({ + page + }) => { + const target = icon(page, 'persistent-info-circle'); + const tooltip = page.getByRole('tooltip', { + name: /This tooltip stays open/ + }); + + await tabUntilFocused(page, target); + await expect(tooltip).toBeVisible(); + + await page.keyboard.press('Tab'); + const link = tooltip.getByRole('link', { name: 'Learn more' }); + await expect(link).toBeFocused(); + }); + }); +}); diff --git a/projects/composition/src/app/pages/info-circle-page/info-circle-page.component.html b/projects/composition/src/app/pages/info-circle-page/info-circle-page.component.html index 43cd9afab..efafa8b11 100644 --- a/projects/composition/src/app/pages/info-circle-page/info-circle-page.component.html +++ b/projects/composition/src/app/pages/info-circle-page/info-circle-page.component.html @@ -4,6 +4,7 @@ label="XSmall info circle with a tooltip at the top" [htmlCode]="examples.xsmallTopTooltip.html"> @@ -13,6 +14,7 @@ label="Small info circle with a tooltip at the right" [htmlCode]="examples.smallRightTooltip.html"> @@ -22,6 +24,7 @@ label="Normal info circle with a tooltip at the left" [htmlCode]="examples.normalLeftTooltip.html"> @@ -31,8 +34,20 @@ label="Large info circle with a tooltip at the bottom" [htmlCode]="examples.largeBottomTooltip.html"> + + + + diff --git a/projects/composition/src/app/pages/info-circle-page/info-circle-page.examples.ts b/projects/composition/src/app/pages/info-circle-page/info-circle-page.examples.ts index 9857f738c..ff4401e59 100644 --- a/projects/composition/src/app/pages/info-circle-page/info-circle-page.examples.ts +++ b/projects/composition/src/app/pages/info-circle-page/info-circle-page.examples.ts @@ -30,5 +30,14 @@ export const infoCircleExamples: Record = size="large" tooltipPosition="bottom" tooltipText="Provide any information here">` + }, + + persistentTooltip: { + html: ` +` } }; diff --git a/projects/cps-ui-kit/src/lib/components/cps-info-circle/cps-info-circle.component.html b/projects/cps-ui-kit/src/lib/components/cps-info-circle/cps-info-circle.component.html index 0343b72ff..48574932a 100644 --- a/projects/cps-ui-kit/src/lib/components/cps-info-circle/cps-info-circle.component.html +++ b/projects/cps-ui-kit/src/lib/components/cps-info-circle/cps-info-circle.component.html @@ -1,5 +1,6 @@ Date: Thu, 30 Jul 2026 12:41:26 +0200 Subject: [PATCH 2/2] address feedback --- .../app/pages/info-circle-page/info-circle-page.component.html | 2 +- .../src/app/pages/info-circle-page/info-circle-page.examples.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/composition/src/app/pages/info-circle-page/info-circle-page.component.html b/projects/composition/src/app/pages/info-circle-page/info-circle-page.component.html index efafa8b11..24fc14740 100644 --- a/projects/composition/src/app/pages/info-circle-page/info-circle-page.component.html +++ b/projects/composition/src/app/pages/info-circle-page/info-circle-page.component.html @@ -48,6 +48,6 @@ size="normal" tooltipPosition="top" [tooltipPersistent]="true" - tooltipText="This tooltip stays open until you click elsewhere. Learn more"> + tooltipText="This tooltip stays open until you click elsewhere. Learn more"> diff --git a/projects/composition/src/app/pages/info-circle-page/info-circle-page.examples.ts b/projects/composition/src/app/pages/info-circle-page/info-circle-page.examples.ts index ff4401e59..89fdb162f 100644 --- a/projects/composition/src/app/pages/info-circle-page/info-circle-page.examples.ts +++ b/projects/composition/src/app/pages/info-circle-page/info-circle-page.examples.ts @@ -38,6 +38,6 @@ export const infoCircleExamples: Record = size="normal" tooltipPosition="top" [tooltipPersistent]="true" - tooltipText="This tooltip stays open until you click elsewhere. Learn more">` + tooltipText="This tooltip stays open until you click elsewhere. Learn more">` } };