diff --git a/code/addons/test/src/components/Description.tsx b/code/addons/test/src/components/Description.tsx index a94b775b5c95..a2bcfe6ae644 100644 --- a/code/addons/test/src/components/Description.tsx +++ b/code/addons/test/src/components/Description.tsx @@ -60,10 +60,10 @@ export function Description({ state, ...props }: DescriptionProps) { ); } else if (state.progress?.finishedAt) { description = ( - + <> + Ran {state.progress.numTotalTests} {state.progress.numTotalTests === 1 ? 'test' : 'tests'}{' '} + + ); } else if (state.watching) { description = 'Watching for file changes'; diff --git a/code/addons/test/src/components/RelativeTime.stories.tsx b/code/addons/test/src/components/RelativeTime.stories.tsx new file mode 100644 index 000000000000..4d3c6af0f6d8 --- /dev/null +++ b/code/addons/test/src/components/RelativeTime.stories.tsx @@ -0,0 +1,47 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { RelativeTime } from './RelativeTime'; + +const meta = { + component: RelativeTime, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const JustNow: Story = { + args: { + timestamp: Date.now() - 1000 * 10, + }, +}; + +export const AMinuteAgo: Story = { + args: { + timestamp: Date.now() - 1000 * 60, + }, +}; + +export const MinutesAgo: Story = { + args: { + timestamp: Date.now() - 1000 * 60 * 2, + }, +}; + +export const HoursAgo: Story = { + args: { + timestamp: Date.now() - 1000 * 60 * 60 * 3, + }, +}; + +export const Yesterday: Story = { + args: { + timestamp: Date.now() - 1000 * 60 * 60 * 24, + }, +}; + +export const DaysAgo: Story = { + args: { + timestamp: Date.now() - 1000 * 60 * 60 * 24 * 3, + }, +}; diff --git a/code/addons/test/src/components/RelativeTime.tsx b/code/addons/test/src/components/RelativeTime.tsx index fa9e7cf6d549..9cb1df1b1b66 100644 --- a/code/addons/test/src/components/RelativeTime.tsx +++ b/code/addons/test/src/components/RelativeTime.tsx @@ -1,41 +1,35 @@ import { useEffect, useState } from 'react'; -export function getRelativeTimeString(date: Date): string { - const delta = Math.round((date.getTime() - Date.now()) / 1000); - const cutoffs = [60, 3600, 86400, 86400 * 7, 86400 * 30, 86400 * 365, Infinity]; - const units: Intl.RelativeTimeFormatUnit[] = [ - 'second', - 'minute', - 'hour', - 'day', - 'week', - 'month', - 'year', - ]; - - const unitIndex = cutoffs.findIndex((cutoff) => cutoff > Math.abs(delta)); - const divisor = unitIndex ? cutoffs[unitIndex - 1] : 1; - const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' }); - return rtf.format(Math.floor(delta / divisor), units[unitIndex]); -} - -export const RelativeTime = ({ timestamp, testCount }: { timestamp: Date; testCount: number }) => { - const [relativeTimeString, setRelativeTimeString] = useState(null); +export const RelativeTime = ({ timestamp }: { timestamp?: number }) => { + const [timeAgo, setTimeAgo] = useState(null); useEffect(() => { if (timestamp) { - setRelativeTimeString(getRelativeTimeString(timestamp).replace(/^now$/, 'just now')); - - const interval = setInterval(() => { - setRelativeTimeString(getRelativeTimeString(timestamp).replace(/^now$/, 'just now')); - }, 10000); - + setTimeAgo(Date.now() - timestamp); + const interval = setInterval(() => setTimeAgo(Date.now() - timestamp), 10000); return () => clearInterval(interval); } }, [timestamp]); - return ( - relativeTimeString && - `Ran ${testCount} ${testCount === 1 ? 'test' : 'tests'} ${relativeTimeString}` - ); + if (timeAgo === null) { + return null; + } + + const seconds = Math.round(timeAgo / 1000); + if (seconds < 60) { + return `just now`; + } + + const minutes = Math.floor(seconds / 60); + if (minutes < 60) { + return minutes === 1 ? `a minute ago` : `${minutes} minutes ago`; + } + + const hours = Math.floor(minutes / 60); + if (hours < 24) { + return hours === 1 ? `an hour ago` : `${hours} hours ago`; + } + + const days = Math.floor(hours / 24); + return days === 1 ? `yesterday` : `${days} days ago`; }; diff --git a/code/core/src/manager/components/sidebar/useHighlighted.ts b/code/core/src/manager/components/sidebar/useHighlighted.ts index d0f06fc87aa8..37701c7b1eec 100644 --- a/code/core/src/manager/components/sidebar/useHighlighted.ts +++ b/code/core/src/manager/components/sidebar/useHighlighted.ts @@ -22,6 +22,25 @@ export interface HighlightedProps { const fromSelection = (selection: Selection): Highlight => selection ? { itemId: selection.storyId, refId: selection.refId } : null; +const scrollToSelector = ( + selector: string, + options: { + containerRef?: RefObject; + center?: boolean; + attempts?: number; + delay?: number; + } = {}, + _attempt = 1 +) => { + const { containerRef, center = false, attempts = 3, delay = 500 } = options; + const element = (containerRef ? containerRef.current : document)?.querySelector(selector); + if (element) { + scrollIntoView(element, center); + } else if (_attempt <= attempts) { + setTimeout(scrollToSelector, delay, selector, options, _attempt + 1); + } +}; + export const useHighlighted = ({ containerRef, isLoading, @@ -65,14 +84,10 @@ export const useHighlighted = ({ const highlight = fromSelection(selected); updateHighlighted(highlight); if (highlight) { - const { itemId, refId } = highlight; - setTimeout(() => { - scrollIntoView( - // @ts-expect-error (non strict) - containerRef.current?.querySelector(`[data-item-id="${itemId}"][data-ref-id="${refId}"]`), - true // make sure it's clearly visible by centering it - ); - }, 0); + scrollToSelector(`[data-item-id="${highlight.itemId}"][data-ref-id="${highlight.refId}"]`, { + containerRef, + center: true, + }); } }, [containerRef, selected, updateHighlighted]); diff --git a/code/core/src/manager/utils/tree.ts b/code/core/src/manager/utils/tree.ts index 3002eb97a77f..dfe6dfb4cf24 100644 --- a/code/core/src/manager/utils/tree.ts +++ b/code/core/src/manager/utils/tree.ts @@ -85,10 +85,14 @@ export const scrollIntoView = (element: Element, center = false) => { return; } const { top, bottom } = element.getBoundingClientRect(); - const isInView = - top >= 0 && bottom <= (globalWindow.innerHeight || document.documentElement.clientHeight); - - if (!isInView) { + if (!top || !bottom) { + return; + } + const bottomOffset = + document?.querySelector('#sidebar-bottom-wrapper')?.getBoundingClientRect().top || + globalWindow.innerHeight || + document.documentElement.clientHeight; + if (bottom > bottomOffset) { element.scrollIntoView({ block: center ? 'center' : 'nearest' }); } };