Skip to content

Commit

Permalink
🐛 #172 - fix: fix a bug that could cause the application to crash if …
Browse files Browse the repository at this point in the history
…a future date wassed passed to the timeAgo function
  • Loading branch information
svenvandescheur committed Jul 11, 2024
1 parent aa616f3 commit c3f55ce
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 106 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ jobs:
run: npx playwright install --with-deps
- name: Build Storybook
run: npm run build-storybook
- name: Run jest tests
run: npm run test
- name: Serve Storybook and run tests
run: |
npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { render } from "@testing-library/react";
import * as React from "react";
import { createBrowserRouter } from "react-router-dom";
import { RouterProvider, createBrowserRouter } from "react-router-dom";

import App from "./App";

test("renders app", () => {
createBrowserRouter([
const router = createBrowserRouter([
{
path: "*",
element: <App />,
},
]);
render(<App />);
render(<RouterProvider router={router} />);
});
123 changes: 123 additions & 0 deletions frontend/src/lib/format/date.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { formatDate, timeAgo } from "./date";

describe("formatDate()", () => {
beforeEach(mockDate);
afterEach(unMockDate);

test("formatDate() formats Date object ", () => {
const date = new Date();
expect(formatDate(date)).toBe("15/09/2023");
});

test("formatDate() formats date string ", () => {
const dateString = "1988-08-02";
expect(formatDate(dateString)).toBe("02/08/1988");
});
});

describe("timeAgo()", () => {
beforeEach(mockDate);
afterEach(unMockDate);

test("timeAgo() handles year ", () => {
const yearAgo = new Date("2022-09-15:00:00");
expect(timeAgo(yearAgo)).toBe("1 jaar geleden");
expect(timeAgo(yearAgo, { shortFormat: true })).toBe("1j");

const yearsAgo = new Date("2021-09-15:00:00");
expect(timeAgo(yearsAgo)).toBe("2 jaren geleden");
expect(timeAgo(yearsAgo, { shortFormat: true })).toBe("2j");
});

test("timeAgo() handles month ", () => {
const monthAgo = new Date("2023-08-15:00:00");
expect(timeAgo(monthAgo)).toBe("1 maand geleden");
expect(timeAgo(monthAgo, { shortFormat: true })).toBe("1ma");

const monthsAgo = new Date("2023-07-15:00:00");
expect(timeAgo(monthsAgo)).toBe("2 maanden geleden");
expect(timeAgo(monthsAgo, { shortFormat: true })).toBe("2ma");
});

test("timeAgo() handles day ", () => {
const dayAgo = new Date("2023-09-14:00:00");
expect(timeAgo(dayAgo)).toBe("1 dag geleden");
expect(timeAgo(dayAgo, { shortFormat: true })).toBe("1d");

const daysAgo = new Date("2023-09-13:00:00");
expect(timeAgo(daysAgo)).toBe("2 dagen geleden");
expect(timeAgo(daysAgo, { shortFormat: true })).toBe("2d");
});

test("timeAgo() handles hour ", () => {
const hourAgo = new Date("2023-09-14:23:00");
expect(timeAgo(hourAgo)).toBe("1 uur geleden");
expect(timeAgo(hourAgo, { shortFormat: true })).toBe("1u");

const hoursAgo = new Date("2023-09-14:22:00");
expect(timeAgo(hoursAgo)).toBe("2 uur geleden");
expect(timeAgo(hoursAgo, { shortFormat: true })).toBe("2u");
});

test("timeAgo() handles minute ", () => {
const minuteAgo = new Date("2023-09-14:23:59");
expect(timeAgo(minuteAgo)).toBe("1 minuut geleden");
expect(timeAgo(minuteAgo, { shortFormat: true })).toBe("1m");

const minutesAgo = new Date("2023-09-14:23:58");
expect(timeAgo(minutesAgo)).toBe("2 minuten geleden");
expect(timeAgo(minutesAgo, { shortFormat: true })).toBe("2m");
});

test("timeAgo() handles less than a minute ", () => {
const secondAgo = new Date("2023-09-14:23:59:59");
expect(timeAgo(secondAgo)).toBe("Nu");
expect(timeAgo(secondAgo, { shortFormat: true })).toBe("0m");

const secondsAgo = new Date("2023-09-14:23:59:59");
expect(timeAgo(secondsAgo)).toBe("Nu");
expect(timeAgo(secondsAgo, { shortFormat: true })).toBe("0m");
});

test("timeAgo() interprets future data as now ", () => {
const yearFromNow = new Date("2024-09-15:00:00");
expect(timeAgo(yearFromNow)).toBe("Nu");
expect(timeAgo(yearFromNow, { shortFormat: true })).toBe("0m");
});

test("timeAgo() handles combined scenario ", () => {
const yearAgo = new Date("2022-08-14:23:59:59");
expect(timeAgo(yearAgo)).toBe("1 jaar geleden");
expect(timeAgo(yearAgo, { shortFormat: true })).toBe("1j");

const monthsAgo = new Date("2023-07-13:22:58:58");
expect(timeAgo(monthsAgo)).toBe("2 maanden geleden");
expect(timeAgo(monthsAgo, { shortFormat: true })).toBe("2ma");
});
});

const SYMBOL_NATIVE_DATE = Symbol();

/**
* Mock Date() constructor to default to a predefined date if no value is provided.
*/
function mockDate() {
// @ts-expect-error - set native reference.
window[SYMBOL_NATIVE_DATE] = window.Date;
// @ts-expect-error - set fake implementation
window.Date = function (ds = "2023-09-15:00:00:00") {
// @ts-expect-error - use native reference.
return new window[SYMBOL_NATIVE_DATE](ds);
};
}

/**
* Restores original Date constructor.
*/
function unMockDate() {
// @ts-expect-error - check native implementation
if (window[SYMBOL_NATIVE_DATE]) {
// @ts-expect-error - set native implementation
window.Date = window[SYMBOL_NATIVE_DATE];
}
}
67 changes: 67 additions & 0 deletions frontend/src/lib/format/date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,70 @@ export function formatDate(date: Date | string) {

return `${day}/${month}/${year}`;
}

/**
* Options for customizing the output of the timeAgo function.
*/
interface TimeAgoOptions {
shortFormat?: boolean; // Use short format like "10d" instead of "10 days ago"
includeSeconds?: boolean; // Include seconds in the output if less than a minute
}

/**
* Calculate how long ago a given date was and return a human-readable string.
* TODO: Consider using a specialized library.
*
* @param dateInput - The date to calculate the time difference from. It can be a Date object or an ISO 8601 string.
* @param options - Customization options for the output.
* @returns A human-readable string indicating how long ago the date was.
*/
export function timeAgo(
dateInput: Date | string,
options: TimeAgoOptions = {},
): string {
// Convert the input to a Date object if it's a string
const date = typeof dateInput === "string" ? new Date(dateInput) : dateInput;

// Check for invalid date input
if (isNaN(date.getTime())) {
throw new Error("Invalid date input");
}

const now = new Date();
// Calculate the difference in seconds between the current date and the input date
let seconds = Math.floor((now.getTime() - date.getTime()) / 1000);
const { shortFormat = false } = options;

// Define the intervals in seconds for various time units
const intervals = [
{ label: "jaar", plural: "jaren", shortFormat: "j", seconds: 31536000 },
{ label: "maand", plural: "maanden", shortFormat: "ma", seconds: 2592000 },
{ label: "week", plural: "weken", shortFormat: "w", seconds: 604800 },
{ label: "dag", plural: "dagen", shortFormat: "d", seconds: 86400 },
{ label: "uur", plural: "uur", shortFormat: "u", seconds: 3600 },
{ label: "minuut", plural: "minuten", shortFormat: "m", seconds: 60 },
];

let result = "";

// Iterate over the intervals to determine the appropriate time unit
for (const interval of intervals) {
const intervalCount = Math.floor(seconds / interval.seconds);
if (intervalCount >= 1) {
// Format the label based on short or long format
const label = shortFormat
? interval.shortFormat
: intervalCount === 1
? interval.label
: interval.plural;

result += `${intervalCount}${shortFormat ? "" : " "}${label}${shortFormat ? "" : " geleden"}`;
// Update seconds to the remainder for the next interval
seconds %= interval.seconds;
break;
}
}

// Return the result or default to "just now" or "0m" for short format
return result.trim() || (shortFormat ? "0m" : "Nu");
}
102 changes: 0 additions & 102 deletions frontend/src/lib/string.ts

This file was deleted.

2 changes: 1 addition & 1 deletion frontend/src/pages/landing/Landing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
canReviewDestructionList,
canUpdateDestructionList,
} from "../../lib/auth/permissions";
import { timeAgo } from "../../lib/string";
import { timeAgo } from "../../lib/format/date";
import { STATUS_MAPPING } from "../destructionlist/detail/constants";
import { formatUser } from "../destructionlist/utils";
import "./Landing.css";
Expand Down

0 comments on commit c3f55ce

Please sign in to comment.