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' });
}
};