mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
fix(site): stabilize date-dependent storybook snapshots (#23657)
_Generated by mux but reviewed by a human_
Several stories computed dates relative to `dayjs()` / `new Date()` at
render time, causing snapshot text to shift daily. I ran into this on my
PRs.
This adds an optional `now` prop to `DateRangePicker`,
`TemplateInsightsControls`, and `CreateTokenForm` so stories can inject
a deterministic clock without global mocking. License stories replace
the misleadingly-named `FIXED_NOW = dayjs().startOf("day")` with
absolute timestamps. All fixed timestamps use noon UTC to avoid timezone
boundary issues.
Affected stories:
- `AgentSettingsPageView`: Usage Date Filter, Usage Date Filter Refetch
Overlay
- `LicenseCard`: Expired/future AI Governance variants, Not Yet Valid
- `LicensesSettingsPage`: Shows Addon Ui For Future License Before Nbf
- `TemplateInsightsControls`: Day
- `CreateTokenPage`: Default
This commit is contained in:
@@ -274,6 +274,7 @@ const UsageContent: FC<UsageContentProps> = ({ now }) => {
|
||||
<DateRangePicker
|
||||
value={displayDateRange}
|
||||
onChange={onDateRangeChange}
|
||||
now={now?.toDate()}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -14,6 +14,9 @@ const defaultValue: DateRangeValue = {
|
||||
const meta: Meta<typeof DateRangePicker> = {
|
||||
title: "components/DateRangePicker",
|
||||
component: DateRangePicker,
|
||||
args: {
|
||||
now: fixedNow.toDate(),
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
@@ -61,7 +64,13 @@ export const Open: Story = {
|
||||
export const SelectPreset: Story = {
|
||||
render: function SelectPresetStory() {
|
||||
const [value, setValue] = useState<DateRangeValue>(defaultValue);
|
||||
return <DateRangePicker value={value} onChange={setValue} />;
|
||||
return (
|
||||
<DateRangePicker
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
now={fixedNow.toDate()}
|
||||
/>
|
||||
);
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
@@ -93,7 +102,13 @@ export const SelectCalendarRange: Story = {
|
||||
startDate: new Date("2025-03-01"),
|
||||
endDate: new Date("2025-03-15"),
|
||||
});
|
||||
return <DateRangePicker value={value} onChange={setValue} />;
|
||||
return (
|
||||
<DateRangePicker
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
now={fixedNow.toDate()}
|
||||
/>
|
||||
);
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
@@ -119,7 +134,13 @@ export const SelectCalendarRange: Story = {
|
||||
export const CancelClosesWithoutApplying: Story = {
|
||||
render: function CancelStory() {
|
||||
const [value, setValue] = useState<DateRangeValue>(defaultValue);
|
||||
return <DateRangePicker value={value} onChange={setValue} />;
|
||||
return (
|
||||
<DateRangePicker
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
now={fixedNow.toDate()}
|
||||
/>
|
||||
);
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
@@ -27,44 +27,60 @@ interface DateRangePreset {
|
||||
range: () => { from: Date; to: Date };
|
||||
}
|
||||
|
||||
const defaultPresets: DateRangePreset[] = [
|
||||
{
|
||||
label: "Today",
|
||||
range: () => ({ from: new Date(), to: new Date() }),
|
||||
},
|
||||
{
|
||||
label: "Yesterday",
|
||||
range: () => {
|
||||
const d = dayjs().subtract(1, "day").toDate();
|
||||
return { from: d, to: d };
|
||||
const buildDefaultPresets = (now?: Date): DateRangePreset[] => {
|
||||
const getCurrentTime = () => dayjs(now ?? new Date());
|
||||
return [
|
||||
{
|
||||
label: "Today",
|
||||
range: () => {
|
||||
const currentTime = getCurrentTime();
|
||||
return { from: currentTime.toDate(), to: currentTime.toDate() };
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Last 7 days",
|
||||
range: () => ({
|
||||
from: dayjs().subtract(6, "day").toDate(),
|
||||
to: new Date(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: "Last 14 days",
|
||||
range: () => ({
|
||||
from: dayjs().subtract(13, "day").toDate(),
|
||||
to: new Date(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: "Last 30 days",
|
||||
range: () => ({
|
||||
from: dayjs().subtract(29, "day").toDate(),
|
||||
to: new Date(),
|
||||
}),
|
||||
},
|
||||
];
|
||||
{
|
||||
label: "Yesterday",
|
||||
range: () => {
|
||||
const d = getCurrentTime().subtract(1, "day").toDate();
|
||||
return { from: d, to: d };
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Last 7 days",
|
||||
range: () => {
|
||||
const currentTime = getCurrentTime();
|
||||
return {
|
||||
from: currentTime.subtract(6, "day").toDate(),
|
||||
to: currentTime.toDate(),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Last 14 days",
|
||||
range: () => {
|
||||
const currentTime = getCurrentTime();
|
||||
return {
|
||||
from: currentTime.subtract(13, "day").toDate(),
|
||||
to: currentTime.toDate(),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Last 30 days",
|
||||
range: () => {
|
||||
const currentTime = getCurrentTime();
|
||||
return {
|
||||
from: currentTime.subtract(29, "day").toDate(),
|
||||
to: currentTime.toDate(),
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
interface DateRangePickerProps {
|
||||
value: DateRangeValue;
|
||||
onChange: (value: DateRangeValue) => void;
|
||||
now?: Date;
|
||||
presets?: DateRangePreset[];
|
||||
}
|
||||
|
||||
@@ -74,10 +90,11 @@ interface DateRangePickerProps {
|
||||
* rounded up to the next hour (if it falls on today) or to the start of
|
||||
* the following day.
|
||||
*/
|
||||
function toBoundary(from: Date, to: Date): DateRangeValue {
|
||||
function toBoundary(from: Date, to: Date, now: Date): DateRangeValue {
|
||||
const currentTime = dayjs(now);
|
||||
const start = dayjs(from).startOf("day").toDate();
|
||||
const end = dayjs(to).isSame(dayjs(), "day")
|
||||
? dayjs().startOf("hour").add(1, "hour").toDate()
|
||||
const end = dayjs(to).isSame(currentTime, "day")
|
||||
? currentTime.startOf("hour").add(1, "hour").toDate()
|
||||
: dayjs(to).startOf("day").add(1, "day").toDate();
|
||||
return { startDate: start, endDate: end };
|
||||
}
|
||||
@@ -100,9 +117,12 @@ function fromBoundary(value: DateRangeValue): DayPickerDateRange {
|
||||
export const DateRangePicker: FC<DateRangePickerProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
presets = defaultPresets,
|
||||
now,
|
||||
presets,
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const currentTime = now ?? new Date();
|
||||
const resolvedPresets = presets ?? buildDefaultPresets(now);
|
||||
|
||||
// Internal selection state kept separate from the committed value
|
||||
// so the user can freely adjust the range before applying. This
|
||||
@@ -113,7 +133,7 @@ export const DateRangePicker: FC<DateRangePickerProps> = ({
|
||||
|
||||
const commit = () => {
|
||||
if (selection?.from && selection?.to) {
|
||||
onChange(toBoundary(selection.from, selection.to));
|
||||
onChange(toBoundary(selection.from, selection.to, now ?? new Date()));
|
||||
}
|
||||
setOpen(false);
|
||||
};
|
||||
@@ -122,7 +142,7 @@ export const DateRangePicker: FC<DateRangePickerProps> = ({
|
||||
const { from, to } = preset.range();
|
||||
setSelection({ from, to });
|
||||
// Presets are a complete selection — commit immediately.
|
||||
onChange(toBoundary(from, to));
|
||||
onChange(toBoundary(from, to, now ?? new Date()));
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
@@ -168,7 +188,7 @@ export const DateRangePicker: FC<DateRangePickerProps> = ({
|
||||
<div className="flex">
|
||||
{/* Presets sidebar */}
|
||||
<div className="flex flex-col border-r border-border-default p-2 text-sm">
|
||||
{presets.map((preset) => (
|
||||
{resolvedPresets.map((preset) => (
|
||||
<button
|
||||
key={preset.label}
|
||||
type="button"
|
||||
@@ -223,7 +243,7 @@ export const DateRangePicker: FC<DateRangePickerProps> = ({
|
||||
selected={selection}
|
||||
onSelect={handleCalendarSelect}
|
||||
numberOfMonths={2}
|
||||
disabled={{ after: new Date() }}
|
||||
disabled={{ after: currentTime }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ interface CreateTokenFormProps {
|
||||
setFormError: (arg0: unknown) => void;
|
||||
isCreating: boolean;
|
||||
creationFailed: boolean;
|
||||
now?: Date;
|
||||
}
|
||||
|
||||
export const CreateTokenForm: FC<CreateTokenFormProps> = ({
|
||||
@@ -42,6 +43,7 @@ export const CreateTokenForm: FC<CreateTokenFormProps> = ({
|
||||
setFormError,
|
||||
isCreating,
|
||||
creationFailed,
|
||||
now,
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -49,6 +51,7 @@ export const CreateTokenForm: FC<CreateTokenFormProps> = ({
|
||||
const [lifetimeDays, setLifetimeDays] = useState<number | string>(
|
||||
determineDefaultLtValue(maxTokenLifetime),
|
||||
);
|
||||
const currentTime = dayjs(now ?? new Date());
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: adding form will cause an infinite loop
|
||||
useEffect(() => {
|
||||
@@ -86,7 +89,7 @@ export const CreateTokenForm: FC<CreateTokenFormProps> = ({
|
||||
<>
|
||||
The token will expire on{" "}
|
||||
<span data-chromatic="ignore">
|
||||
{dayjs()
|
||||
{currentTime
|
||||
.add(form.values.lifetime, "days")
|
||||
.utc()
|
||||
.format("MMMM DD, YYYY")}
|
||||
|
||||
@@ -17,4 +17,8 @@ const meta: Meta<typeof CreateTokenPage> = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof CreateTokenPage>;
|
||||
|
||||
export const Default: Story = {};
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
now: new Date("2026-03-12T12:00:00Z"),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -19,7 +19,11 @@ const initialValues: CreateTokenData = {
|
||||
lifetime: 30,
|
||||
};
|
||||
|
||||
const CreateTokenPage: FC = () => {
|
||||
type CreateTokenPageProps = {
|
||||
now?: Date;
|
||||
};
|
||||
|
||||
const CreateTokenPage: FC<CreateTokenPageProps> = ({ now }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {
|
||||
@@ -105,6 +109,7 @@ const CreateTokenPage: FC = () => {
|
||||
setFormError={setFormError}
|
||||
isCreating={isCreating}
|
||||
creationFailed={creationFailed}
|
||||
now={now}
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
|
||||
@@ -6,9 +6,8 @@ import { expect, fn, within } from "storybook/test";
|
||||
|
||||
import { LicenseCard } from "./LicenseCard";
|
||||
|
||||
const FIXED_NOW = dayjs().startOf("day");
|
||||
const YESTERDAY = FIXED_NOW.subtract(1, "day").unix();
|
||||
const NEXT_WEEK = FIXED_NOW.add(7, "day").unix();
|
||||
const EXPIRED_DATE = dayjs("2000-01-01T12:00:00Z").unix();
|
||||
const FUTURE_START_DATE = dayjs("2099-01-01T12:00:00Z").unix();
|
||||
|
||||
const meta: Meta<typeof LicenseCard> = {
|
||||
title: "pages/DeploymentSettingsPage/LicensesSettingsPage/LicenseCard",
|
||||
@@ -176,7 +175,7 @@ export const ExpiredAIGovernanceOverageShowsExpired: Story = {
|
||||
...MockLicenseResponse[1],
|
||||
claims: {
|
||||
...MockLicenseResponse[1].claims,
|
||||
license_expires: YESTERDAY,
|
||||
license_expires: EXPIRED_DATE,
|
||||
features: {
|
||||
...MockLicenseResponse[1].claims.features,
|
||||
ai_governance_user_limit: 1000,
|
||||
@@ -208,7 +207,7 @@ export const ExpiredAIGovernanceInGracePeriodShowsExceeded: Story = {
|
||||
...MockLicenseResponse[1],
|
||||
claims: {
|
||||
...MockLicenseResponse[1].claims,
|
||||
license_expires: YESTERDAY,
|
||||
license_expires: EXPIRED_DATE,
|
||||
features: {
|
||||
...MockLicenseResponse[1].claims.features,
|
||||
ai_governance_user_limit: 1000,
|
||||
@@ -238,7 +237,7 @@ export const NotYetValid: Story = {
|
||||
...MockLicenseResponse[1],
|
||||
claims: {
|
||||
...MockLicenseResponse[1].claims,
|
||||
nbf: NEXT_WEEK,
|
||||
nbf: FUTURE_START_DATE,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -254,7 +253,7 @@ export const FutureAIGovernanceOverageShowsStartsOn: Story = {
|
||||
...MockLicenseResponse[1],
|
||||
claims: {
|
||||
...MockLicenseResponse[1].claims,
|
||||
nbf: NEXT_WEEK,
|
||||
nbf: FUTURE_START_DATE,
|
||||
features: {
|
||||
...MockLicenseResponse[1].claims.features,
|
||||
ai_governance_user_limit: 1000,
|
||||
@@ -286,7 +285,7 @@ export const FutureAIGovernanceUsageShowsNoCurrentSeats: Story = {
|
||||
...MockLicenseResponse[1],
|
||||
claims: {
|
||||
...MockLicenseResponse[1].claims,
|
||||
nbf: NEXT_WEEK,
|
||||
nbf: FUTURE_START_DATE,
|
||||
features: {
|
||||
...MockLicenseResponse[1].claims.features,
|
||||
ai_governance_user_limit: 1000,
|
||||
|
||||
+6
-4
@@ -23,6 +23,8 @@ const USER_STATUS_COUNTS_QUERY = {
|
||||
data: { active: [] },
|
||||
};
|
||||
|
||||
const STORY_NOW = dayjs("2099-01-01T12:00:00Z");
|
||||
|
||||
const withBaseQueries = ({
|
||||
entitlements = MockEntitlements,
|
||||
licenses = MockLicenseResponse,
|
||||
@@ -89,8 +91,8 @@ const createLicense = ({
|
||||
addons?: string[];
|
||||
}) => ({
|
||||
id,
|
||||
uploaded_at: String(dayjs().subtract(uploadedDaysAgo, "day").unix()),
|
||||
expires_at: String(dayjs().add(expiresInDays, "day").unix()),
|
||||
uploaded_at: String(STORY_NOW.subtract(uploadedDaysAgo, "day").unix()),
|
||||
expires_at: String(STORY_NOW.add(expiresInDays, "day").unix()),
|
||||
uuid,
|
||||
claims: {
|
||||
trial: false,
|
||||
@@ -102,8 +104,8 @@ const createLicense = ({
|
||||
user_limit: userLimit,
|
||||
},
|
||||
addons,
|
||||
license_expires: dayjs().add(licenseExpiresInDays, "day").unix(),
|
||||
nbf: dayjs().add(nbfOffsetDays, "day").unix(),
|
||||
license_expires: STORY_NOW.add(licenseExpiresInDays, "day").unix(),
|
||||
nbf: STORY_NOW.add(nbfOffsetDays, "day").unix(),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ const defaultArgs: Partial<ComponentProps<typeof TemplateInsightsControls>> = {
|
||||
startDate: new Date("2025-08-05"),
|
||||
endDate: new Date("2025-08-07"),
|
||||
},
|
||||
now: new Date("2025-08-07T12:00:00Z"),
|
||||
setDateRange: () => {},
|
||||
searchParams: new URLSearchParams(),
|
||||
setSearchParams: () => {},
|
||||
|
||||
@@ -142,6 +142,7 @@ interface TemplateInsightsControlsProps {
|
||||
setDateRange: (value: DateRangeValue) => void;
|
||||
searchParams: URLSearchParams;
|
||||
setSearchParams: SetURLSearchParams;
|
||||
now?: Date;
|
||||
}
|
||||
|
||||
export const TemplateInsightsControls: FC<TemplateInsightsControlsProps> = ({
|
||||
@@ -150,6 +151,7 @@ export const TemplateInsightsControls: FC<TemplateInsightsControlsProps> = ({
|
||||
setDateRange,
|
||||
searchParams,
|
||||
setSearchParams,
|
||||
now,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
@@ -165,7 +167,7 @@ export const TemplateInsightsControls: FC<TemplateInsightsControlsProps> = ({
|
||||
}}
|
||||
/>
|
||||
{interval === "day" ? (
|
||||
<DailyPicker value={dateRange} onChange={setDateRange} />
|
||||
<DailyPicker value={dateRange} onChange={setDateRange} now={now} />
|
||||
) : (
|
||||
<WeekPicker value={dateRange} onChange={setDateRange} />
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user