Skip to content

Commit

Permalink
feat: setup base unit and integration testing functionality with Jest
Browse files Browse the repository at this point in the history
  • Loading branch information
danielstals committed Sep 7, 2024
1 parent d3dcefd commit e3a1464
Show file tree
Hide file tree
Showing 16 changed files with 2,995 additions and 123 deletions.
1 change: 0 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ module.exports = {
'plugin:testing-library/react',
'plugin:jest-dom/recommended',
'plugin:tailwindcss/recommended',
'plugin:vitest/legacy-recommended',
],
rules: {
'@next/next/no-img-element': 'off',
Expand Down
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pnpm lint-staged
1 change: 1 addition & 0 deletions __mocks__/jest-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="@types/jest" />
13 changes: 13 additions & 0 deletions __mocks__/next/image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react';

interface ImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
src: string;
alt: string;
}

// Mock Next.js `Image` component using a standard `img` element
const NextImage: React.FC<ImageProps> = ({ src, alt, ...props }) => {
return <img src={src} alt={alt} {...props} />;
};

export default NextImage;
16 changes: 16 additions & 0 deletions __mocks__/next/link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as React from 'react';

type LinkProps = React.AnchorHTMLAttributes<HTMLAnchorElement> & {
href: string;
};

// Mock Next.js `Link` component using a standard `a` element
const Link: React.FC<LinkProps> = ({ href, children, ...props }) => {
return (
<a data-testid='link-mock' href={href} {...props}>
{children}
</a>
);
};

export default Link;
28 changes: 28 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Config } from 'jest';
import nextJest from 'next/jest';

const createJestConfig = nextJest({
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
dir: './',
});

const config: Config = {
// Automatically clear mock calls, instances, contexts and results before every test
clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test
collectCoverage: true,
// The directory where Jest should output its coverage files
coverageDirectory: 'coverage',
// Indicates which provider should be used to instrument code for coverage
coverageProvider: 'v8',
// A list of paths to modules that run some code to configure or set up the testing framework before each test
setupFilesAfterEnv: ['<rootDir>/src/testing/jest.setup.ts'],
// The test environment that will be used for testing
testEnvironment: 'jsdom',
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
moduleNameMapper: {
'^@/components/(.*)$': '<rootDir>/src/components/$1',
},
};

export default createJestConfig(config);
9 changes: 9 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
"build": "next build",
"start": "next start",
"lint": "next lint",
"test": "jest",
"test:watch": "jest --watch",
"check-types": "tsc --project tsconfig.json --pretty --noEmit",
"generate": "npx ts-node ./scripts/generate.ts",
"generate-upstash": "npx ts-node ./scripts/generate-upstash.ts",
Expand All @@ -24,6 +26,8 @@
"@t3-oss/env-nextjs": "^0.11.0",
"@tailwindcss/typography": "^0.5.15",
"@tanstack/react-query": "^5.51.23",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.5.0",
"@upstash/rag-chat": "^1.5.0",
"@upstash/ratelimit": "^2.0.2",
"@upstash/redis": "^1.34.0",
Expand All @@ -45,13 +49,16 @@
"react-error-boundary": "^4.0.13",
"react-icons": "^5.3.0",
"react-markdown": "^9.0.1",
"sharp": "^0.33.5",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
"uuid": "^10.0.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
"@testing-library/react": "^16.0.1",
"@types/jest": "^29.5.12",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
Expand All @@ -75,6 +82,8 @@
"eslint-plugin-testing-library": "^6.3.0",
"eslint-plugin-vitest": "^0.5.4",
"husky": "^9.1.5",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"lint-staged": "^15.2.10",
"postcss": "^8",
"tailwindcss": "^3.4.1",
Expand Down
2,907 changes: 2,794 additions & 113 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions src/app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { aiUseChatAdapter } from '@upstash/rag-chat/nextjs';
import { NextRequest, NextResponse } from 'next/server';

import { ragChat } from '@/lib/ai/rag-chat';
import { convertUnixToLocalTimeWithDifference } from '@/utils/format';
import { getTimeDifferenceStrUntilUnixTimestamp } from '@/utils/format';

export const POST = async (req: NextRequest) => {
const { messages, sessionId } = await req.json();
Expand All @@ -24,7 +24,9 @@ export const POST = async (req: NextRequest) => {
console.error(error);
if (error instanceof RatelimitUpstashError) {
const unixResetTime: number | undefined = (error.cause as RatelimitResponse)?.resetTime;
const formattedTimeLeft: string | undefined = unixResetTime ? convertUnixToLocalTimeWithDifference(unixResetTime) : undefined;
const formattedTimeLeft: string | null | undefined = unixResetTime
? getTimeDifferenceStrUntilUnixTimestamp(unixResetTime)
: undefined;

return NextResponse.json(
{
Expand Down
26 changes: 26 additions & 0 deletions src/lib/constants/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export const LINKEDIN_URL = 'https://www.linkedin.com/in/danielstals/';

export const CHAT_AVATAR_IMG_ALT = "Daniel's chat avatar";

export const PROMPT_TEMPLATE = (
question: string,
chatHistory: string | undefined,
context: string,
) => `You are a friendly AI assistant augmented with an Upstash Vector Store.
You impersonate the person in the profile.
To help you answer the questions, a context and/or chat history will be provided.
Answer the question at the end using only the information available in the context or chat history, either one is ok.
Answer as completely as possible, and take special care in looking at the dates mentioned in the context.
Whenever it makes sense, provide links to pages that contain more information about the topic from the given context.
Format your messages in markdown format.
-------------
Chat history:
${chatHistory}
-------------
Context:
${context}
-------------
Question: ${question}
Helpful answer:`;
2 changes: 2 additions & 0 deletions src/testing/jest.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
6 changes: 6 additions & 0 deletions src/testing/test-utils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Centralised test utils and re-exports for flexibility and maintainability
import { render as rtlRender } from '@testing-library/react';

export { rtlRender };

export * from '@testing-library/react';
28 changes: 28 additions & 0 deletions src/utils/__tests__/cn.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { cn } from '../cn';

describe('cn', () => {
it('should return an empty string when no arguments are provided', () => {
const result = cn();
expect(result).toEqual('');
});

it('should concatenate multiple class names into a single string', () => {
const result = cn('class1', 'class2', 'class3');
expect(result).toEqual('class1 class2 class3');
});

it('should ignore falsy values', () => {
const result = cn('class1', null, undefined, 'class2', false, 'class3');
expect(result).toEqual('class1 class2 class3');
});

it('should handle arrays of class names', () => {
const result = cn(['class1', 'class2'], ['class3', 'class4']);
expect(result).toEqual('class1 class2 class3 class4');
});

it('should handle objects with class names as keys', () => {
const result = cn({ class1: true, class2: false, class3: true });
expect(result).toEqual('class1 class3');
});
});
59 changes: 59 additions & 0 deletions src/utils/__tests__/format.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { getTimeDifferenceStrUntilUnixTimestamp } from '../format';

describe('getTimeDifferenceStrUntilUnixTimestamp', () => {
it('should return the correct time difference string', () => {
// Use a timestamp that represents a time difference of 1 day, 2 hours, 3 minutes, and 4 seconds from now
const now = Date.now();
const oneDayInMs = 24 * 60 * 60 * 1000;
const twoHoursInMs = 2 * 60 * 60 * 1000;
const threeMinutesInMs = 3 * 60 * 1000;
const fourSecondsInMs = 4 * 1000;
const unixTimestamp = now + oneDayInMs + twoHoursInMs + threeMinutesInMs + fourSecondsInMs;

const expected = 'Je kunt weer berichten sturen over 1 dagen, 2 uren, 3 minuten, 4 seconden';

const result = getTimeDifferenceStrUntilUnixTimestamp(unixTimestamp);

expect(result).toEqual(expected);
});

it('should adapt the time differene string when certain time units are 0', () => {
// Use a timestamp that represents a time difference of 2 hours and 30 minutes from now
const now = Date.now();
const twoHoursInMs = 2 * 60 * 60 * 1000;
const thirtyMinutesInMs = 30 * 60 * 1000;
const unixTimestamp = now + twoHoursInMs + thirtyMinutesInMs;

const expected = 'Je kunt weer berichten sturen over 2 uren, 30 minuten';

const result = getTimeDifferenceStrUntilUnixTimestamp(unixTimestamp);

expect(result).toEqual(expected);
});

it('should return null when the time difference is 0', () => {
// Use a timestamp that represents the current time
const now = Date.now();
const unixTimestamp = now;

const result = getTimeDifferenceStrUntilUnixTimestamp(unixTimestamp);

expect(result).toEqual(null);
});

it('should return null difference string when the timestamp is in the past', () => {
// Use a timestamp that represents a time difference of 1 day, 2 hours, 3 minutes, and 4 seconds before now

const now = Date.now();
const oneDayInMs = 24 * 60 * 60 * 1000;
const twoHoursInMs = 2 * 60 * 60 * 1000;
const threeMinutesInMs = 3 * 60 * 1000;
const fourSecondsInMs = 4 * 1000;

const unixTimestamp = now - oneDayInMs - twoHoursInMs - threeMinutesInMs - fourSecondsInMs;

const result = getTimeDifferenceStrUntilUnixTimestamp(unixTimestamp);

expect(result).toEqual(null);
});
});
8 changes: 7 additions & 1 deletion src/utils/format.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
import { differenceInDays, differenceInHours, differenceInMinutes, differenceInSeconds } from 'date-fns';

export function convertUnixToLocalTimeWithDifference(unixTimestamp: number): string {
export function getTimeDifferenceStrUntilUnixTimestamp(unixTimestamp: number): string | null {
// Convert the Unix timestamp from milliseconds to a JavaScript Date object
const date = new Date(unixTimestamp);

// Get the current time
const now = new Date();

// Return null if the timestamp is in the past
if (date < now) return null;

// Calculate the absolute difference in time units
const days = Math.abs(differenceInDays(date, now));
const hours = Math.abs(differenceInHours(date, now) % 24);
const minutes = Math.abs(differenceInMinutes(date, now) % 60);
const seconds = Math.abs(differenceInSeconds(date, now) % 60);

// Return null if the time difference is 0
if (days === 0 && hours === 0 && minutes === 0 && seconds === 0) return null;

// Construct the time difference string
let timeDifference = 'Je kunt weer berichten sturen over ';

Expand Down
7 changes: 1 addition & 6 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,5 @@
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"],
"ts-node": {
"compilerOptions": {
"module": "commonjs"
}
}
"exclude": ["node_modules"]
}

0 comments on commit e3a1464

Please sign in to comment.