Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
Original file line number Diff line number Diff line change
@@ -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');
}
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -169,14 +170,9 @@
</a>
</Table.Cell>
<Table.Cell class="p-0">
<a
aria-label={`Open event ${sessionEvent.id}`}
class="text-foreground block p-2 no-underline"
href={eventHref}
title="Open event details"
>
<A aria-label={`Open event ${sessionEvent.id}`} class="text-foreground block p-2" href={eventHref} variant="ghost">
<TimeAgo value={sessionEvent.date} />
</a>
</A>
</Table.Cell>
</Table.Row>
{/each}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@
}

let { value }: Props = $props();

const title = $derived(
value
? new Date(value).toLocaleString(undefined, {
dateStyle: 'medium',
timeStyle: 'long'
})
: undefined
);
</script>

<Time live={true} relative={true} timestamp={value}></Time>
<Time {title} live={true} relative={true} timestamp={value}>
{#snippet children(relativeTime)}
{relativeTime}
{#if title}<span class="sr-only"> ({title})</span>{/if}
{/snippet}
</Time>
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,47 @@ describe('TimeAgo', () => {
vi.useRealTimers();
});

it.each([new Date(2026, 7, 11, 12, 34, 56), '2026-08-11T12:34:56'])('includes the full local timestamp in the native hover title', (value) => {
vi.useFakeTimers();
vi.setSystemTime(new Date(2026, 7, 11, 12, 35, 56));
const { container } = render(TimeAgo, { value });
const time = container.querySelector('time');
expect(time?.textContent).toContain('a minute ago');
expect(time?.title).toBe(new Date(value).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'long' }));
expect(time?.querySelector('.sr-only')?.textContent).toContain(time!.title);
expect(time?.hasAttribute('tabindex')).toBe(false);
});

it('keeps midnight and zero seconds in the hover title', () => {
const { container } = render(TimeAgo, { value: new Date(2026, 0, 2, 0, 0, 0) });
expect(container.querySelector('time')?.title).toBe(
new Date(2026, 0, 2, 0, 0, 0).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'long' })
);
});

it('updates the hover title when the timestamp changes', async () => {
const { container, rerender } = render(TimeAgo, { value: new Date(2026, 0, 2, 0, 0, 0) });
await rerender({ value: new Date(2026, 0, 3, 13, 4, 5) });
expect(container.querySelector('time')?.title).toBe(
new Date(2026, 0, 3, 13, 4, 5).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'long' })
);
});

it('does not reformat the full timestamp when the relative clock ticks', async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-08-11T12:35:56Z'));
const format = vi.spyOn(Date.prototype, 'toLocaleString');
try {
render(TimeAgo, { value: '2026-08-11T12:34:56Z' });
await tick();
expect(format).toHaveBeenCalledExactlyOnceWith(undefined, { dateStyle: 'medium', timeStyle: 'long' });
await vi.advanceTimersByTimeAsync(60_000);
expect(format).toHaveBeenCalledTimes(1);
} finally {
format.mockRestore();
}
});

it('does not loop when adaptive clocks straddle an age boundary', async () => {
vi.useFakeTimers();
const base = new Date('2026-08-11T12:00:00Z');
Expand All @@ -35,6 +76,6 @@ describe('TimeAgo', () => {
});
await tick();

expect(screen.getByText('an hour ago')).toBeTruthy();
expect(screen.getByText(/an hour ago/)).toBeTruthy();
});
});
Loading