diff --git a/src/Exceptionless.Web/ClientApp/e2e/tests/session-investigation.e2e.ts b/src/Exceptionless.Web/ClientApp/e2e/tests/session-investigation.e2e.ts index 09f93c49b7..3181bb9f4b 100644 --- a/src/Exceptionless.Web/ClientApp/e2e/tests/session-investigation.e2e.ts +++ b/src/Exceptionless.Web/ClientApp/e2e/tests/session-investigation.e2e.ts @@ -98,6 +98,20 @@ test('operator can find and inspect a user session', async ({ e2eApi, e2eScenari expect(eventsUrl.searchParams.get('time')).toBe('all'); }); + await test.step('timestamp remains inside a single navigable link with its own hover title', async () => { + const link = page.getByRole('link', { exact: true, name: `Open event ${relatedEventId}` }); + const time = link.locator('time'); + await time.hover(); + await expect(time).toHaveAttribute('title', /.+/); + await expect(time.locator('.sr-only')).toContainText((await time.getAttribute('title'))!); + await expect(time).not.toHaveAttribute('tabindex'); + await link.locator('xpath=ancestor::tr').getByRole('link').nth(1).focus(); + await page.keyboard.press('Tab'); + await expect(link).toBeFocused(); + await page.keyboard.press('Enter'); + await expect(page).toHaveURL(new RegExp(`/event/${relatedEventId}(?:[?#]|$)`)); + }); + await test.step('event detail messages wrap and small alignment fixes render consistently', async () => { await page.goto(`/next/stack/${relatedStackId}/event/${relatedEventId}`); await expect(page.getByRole('tab', { name: 'Overview' })).toHaveAttribute('aria-selected', 'true'); diff --git a/src/Exceptionless.Web/ClientApp/e2e/tests/timestamp-tooltips.e2e.ts b/src/Exceptionless.Web/ClientApp/e2e/tests/timestamp-tooltips.e2e.ts new file mode 100644 index 0000000000..3ca11a6af0 --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/e2e/tests/timestamp-tooltips.e2e.ts @@ -0,0 +1,46 @@ +import { expect, test } from '../fixtures/e2e-test'; + +for (const [locale, timezoneId] of [ + ['en-US', 'America/Los_Angeles'], + ['en-GB', 'Europe/London'], + ['de-DE', 'Europe/Berlin'], + ['ja-JP', 'Asia/Tokyo'], + ['ar-EG', 'Africa/Cairo'] +]) { + test.describe(`${locale} in ${timezoneId}`, () => { + test.use({ locale, timezoneId }); + + test('event and stack timestamps include the full date in the native hover title', async ({ e2eApi, e2eScenario, page }) => { + const date = new Date(); + date.setUTCHours(0, 0, 0, 0); + await e2eApi.submitEvent(e2eScenario.projectId, e2eScenario.projectToken, { + date: date.toISOString(), + message: 'Synthetic timestamp verification', + reference_id: e2eScenario.referenceId, + type: 'error' + }); + await e2eApi.pollForEventByReference(e2eScenario.userToken, e2eScenario.projectId, e2eScenario.referenceId); + + for (const route of ['/next/event?time=all', '/next/stack?time=all']) { + await page.goto(route); + const timestamp = page.locator('time').first(); + await expect(timestamp).toBeVisible(); + await timestamp.hover(); + const expected = await page.evaluate( + ({ locale, timezoneId, value }) => + new Intl.DateTimeFormat(locale, { + dateStyle: 'medium', + timeStyle: 'long', + timeZone: timezoneId + }).format(new Date(value)), + { locale, timezoneId, value: date.toISOString() } + ); + await expect(timestamp).toHaveAttribute('title', expected); + await expect(timestamp.locator('.sr-only')).toContainText(expected); + const accessibleText = await timestamp.ariaSnapshot(); + expect(accessibleText).toContain(expected); + await expect(timestamp).not.toHaveAttribute('tabindex'); + } + }); + }); +} diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/views/session-events.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/views/session-events.svelte index 6a25a93e7e..a53a99b6ae 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/views/session-events.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/views/session-events.svelte @@ -2,6 +2,7 @@ import type { PersistentEvent } from '$features/events/models'; import TimeAgo from '$comp/formatters/time-ago.svelte'; + import { A } from '$comp/typography'; import * as Alert from '$comp/ui/alert'; import { Button } from '$comp/ui/button'; import { Skeleton } from '$comp/ui/skeleton'; @@ -169,14 +170,9 @@