From 8e2fb5c858f06e1740cfdf9e8c28ee8d82b6fff5 Mon Sep 17 00:00:00 2001 From: TestKraft Bot Date: Mon, 30 Mar 2026 13:26:29 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=96=20Add=20comprehensive=20unit=20tes?= =?UTF-8?q?ts=20via=20TestKraft?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generated comprehensive unit tests for frontend components: - layout.test.tsx: Tests for RootLayout component - page.test.tsx: Tests for Home page component - jest.config.js: Jest configuration for Next.js - jest.setup.js: Jest setup with testing-library/jest-dom Co-Authored-By: Claude Sonnet 4.5 --- frontend/jest.config.js | 28 +++ frontend/jest.setup.js | 2 + frontend/src/app/layout.test.tsx | 254 +++++++++++++++++++ frontend/src/app/page.test.tsx | 420 +++++++++++++++++++++++++++++++ 4 files changed, 704 insertions(+) create mode 100644 frontend/jest.config.js create mode 100644 frontend/jest.setup.js create mode 100644 frontend/src/app/layout.test.tsx create mode 100644 frontend/src/app/page.test.tsx diff --git a/frontend/jest.config.js b/frontend/jest.config.js new file mode 100644 index 0000000..bcc8ed2 --- /dev/null +++ b/frontend/jest.config.js @@ -0,0 +1,28 @@ +const nextJest = require('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: './', +}); + +// Add any custom config to be passed to Jest +const customJestConfig = { + setupFilesAfterEnv: ['/jest.setup.js'], + testEnvironment: 'jest-environment-jsdom', + moduleNameMapper: { + '^@/(.*)$': '/src/$1', + }, + testMatch: [ + '**/__tests__/**/*.[jt]s?(x)', + '**/?(*.)+(spec|test).[jt]s?(x)', + ], + collectCoverageFrom: [ + 'src/**/*.{js,jsx,ts,tsx}', + '!src/**/*.d.ts', + '!src/**/*.test.{js,jsx,ts,tsx}', + '!src/**/*.spec.{js,jsx,ts,tsx}', + ], +}; + +// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async +module.exports = createJestConfig(customJestConfig); diff --git a/frontend/jest.setup.js b/frontend/jest.setup.js new file mode 100644 index 0000000..d15902f --- /dev/null +++ b/frontend/jest.setup.js @@ -0,0 +1,2 @@ +// Learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom'; diff --git a/frontend/src/app/layout.test.tsx b/frontend/src/app/layout.test.tsx new file mode 100644 index 0000000..cd336da --- /dev/null +++ b/frontend/src/app/layout.test.tsx @@ -0,0 +1,254 @@ +import { render, screen } from '@testing-library/react'; +import RootLayout, { metadata } from './layout'; + +// Mock next/font/google +jest.mock('next/font/google', () => ({ + Geist: jest.fn(() => ({ + variable: '--font-geist-sans-mock', + subsets: ['latin'], + })), + Geist_Mono: jest.fn(() => ({ + variable: '--font-geist-mono-mock', + subsets: ['latin'], + })), +})); + +// Mock CSS imports +jest.mock('./globals.css', () => ({})); + +describe('RootLayout Component', () => { + describe('Metadata Export', () => { + it('should export metadata with correct title', () => { + expect(metadata).toBeDefined(); + expect(metadata.title).toBe('Create Next App'); + }); + + it('should export metadata with correct description', () => { + expect(metadata.description).toBe('Generated by create next app'); + }); + + it('should have metadata as an object with required properties', () => { + expect(typeof metadata).toBe('object'); + expect(metadata).toHaveProperty('title'); + expect(metadata).toHaveProperty('description'); + }); + + // Boundary: empty-like values (metadata structure) + it('should have non-empty title string', () => { + expect(typeof metadata.title).toBe('string'); + expect(metadata.title.length).toBeGreaterThan(0); + }); + + it('should have non-empty description string', () => { + expect(typeof metadata.description).toBe('string'); + expect(metadata.description.length).toBeGreaterThan(0); + }); + }); + + describe('Layout Rendering', () => { + it('should render html element with correct lang attribute', () => { + const { container } = render( + +
Test Child
+
+ ); + const htmlElement = container.querySelector('html'); + expect(htmlElement).toBeInTheDocument(); + expect(htmlElement).toHaveAttribute('lang', 'en'); + }); + + it('should render html element with font variables in className', () => { + const { container } = render( + +
Test Child
+
+ ); + const htmlElement = container.querySelector('html'); + expect(htmlElement?.className).toContain('--font-geist-sans-mock'); + expect(htmlElement?.className).toContain('--font-geist-mono-mock'); + }); + + it('should render html element with antialiased class', () => { + const { container } = render( + +
Test Child
+
+ ); + const htmlElement = container.querySelector('html'); + expect(htmlElement?.className).toContain('antialiased'); + }); + + it('should render html element with h-full class', () => { + const { container } = render( + +
Test Child
+
+ ); + const htmlElement = container.querySelector('html'); + expect(htmlElement?.className).toContain('h-full'); + }); + + it('should render body element with correct classes', () => { + const { container } = render( + +
Test Child
+
+ ); + const bodyElement = container.querySelector('body'); + expect(bodyElement).toBeInTheDocument(); + expect(bodyElement).toHaveClass('min-h-full'); + expect(bodyElement).toHaveClass('flex'); + expect(bodyElement).toHaveClass('flex-col'); + }); + + it('should render children inside body element', () => { + render( + +
Test Child Content
+
+ ); + const childElement = screen.getByTestId('test-child'); + expect(childElement).toBeInTheDocument(); + expect(childElement).toHaveTextContent('Test Child Content'); + }); + + // Boundary: null/undefined children handling + it('should handle when children is null', () => { + const { container } = render( + + {null} + + ); + const bodyElement = container.querySelector('body'); + expect(bodyElement).toBeInTheDocument(); + expect(bodyElement?.textContent).toBe(''); + }); + + it('should handle when children is undefined', () => { + const { container } = render( + + {undefined} + + ); + const bodyElement = container.querySelector('body'); + expect(bodyElement).toBeInTheDocument(); + }); + + // Boundary: empty children + it('should handle empty string as children', () => { + render( + + {''} + + ); + const bodyElement = document.querySelector('body'); + expect(bodyElement).toBeInTheDocument(); + }); + + // Equivalence Class: different types of children + it('should render text node children', () => { + render( + + Plain text content + + ); + expect(screen.getByText('Plain text content')).toBeInTheDocument(); + }); + + it('should render multiple children elements', () => { + render( + +
First Child
+
Second Child
+
+ ); + expect(screen.getByTestId('child-1')).toBeInTheDocument(); + expect(screen.getByTestId('child-2')).toBeInTheDocument(); + }); + + it('should render nested children structures', () => { + render( + +
+
Nested Content
+
+
+ ); + expect(screen.getByTestId('parent')).toBeInTheDocument(); + expect(screen.getByTestId('nested-child')).toBeInTheDocument(); + }); + + it('should render fragment children', () => { + render( + + <> +
Fragment Child 1
+
Fragment Child 2
+ +
+ ); + expect(screen.getByTestId('fragment-child-1')).toBeInTheDocument(); + expect(screen.getByTestId('fragment-child-2')).toBeInTheDocument(); + }); + }); + + describe('Structure and Hierarchy', () => { + it('should maintain correct DOM hierarchy: html > body > children', () => { + const { container } = render( + +
Content
+
+ ); + const htmlElement = container.querySelector('html'); + const bodyElement = htmlElement?.querySelector('body'); + const contentElement = bodyElement?.querySelector('[data-testid="content"]'); + + expect(htmlElement).toBeInTheDocument(); + expect(bodyElement).toBeInTheDocument(); + expect(contentElement).toBeInTheDocument(); + }); + + it('should have exactly one body element inside html', () => { + const { container } = render( + +
Content
+
+ ); + const htmlElement = container.querySelector('html'); + const bodyElements = htmlElement?.querySelectorAll('body'); + expect(bodyElements?.length).toBe(1); + }); + }); + + describe('CSS Class Application', () => { + it('should apply all required body classes together', () => { + const { container } = render( + +
Test
+
+ ); + const bodyElement = container.querySelector('body'); + const classList = Array.from(bodyElement?.classList || []); + + expect(classList).toContain('min-h-full'); + expect(classList).toContain('flex'); + expect(classList).toContain('flex-col'); + expect(classList.length).toBe(3); + }); + + it('should apply all required html classes including font variables', () => { + const { container } = render( + +
Test
+
+ ); + const htmlElement = container.querySelector('html'); + const className = htmlElement?.className || ''; + + expect(className).toContain('--font-geist-sans-mock'); + expect(className).toContain('--font-geist-mono-mock'); + expect(className).toContain('h-full'); + expect(className).toContain('antialiased'); + }); + }); +}); diff --git a/frontend/src/app/page.test.tsx b/frontend/src/app/page.test.tsx new file mode 100644 index 0000000..796ecb8 --- /dev/null +++ b/frontend/src/app/page.test.tsx @@ -0,0 +1,420 @@ +import { render, screen } from '@testing-library/react'; +import Home from './page'; + +// Mock next/image +jest.mock('next/image', () => ({ + __esModule: true, + default: (props: any) => { + // eslint-disable-next-line @next/next/no-img-element, jsx-a11y/alt-text + return ; + }, +})); + +describe('Home Component', () => { + describe('Component Rendering', () => { + it('should render the main container div', () => { + const { container } = render(); + const mainContainer = container.querySelector('.flex.flex-col.flex-1'); + expect(mainContainer).toBeInTheDocument(); + }); + + it('should render the main element', () => { + render(); + const mainElement = document.querySelector('main'); + expect(mainElement).toBeInTheDocument(); + }); + + it('should apply correct classes to main container div', () => { + const { container } = render(); + const mainContainer = container.querySelector('div'); + expect(mainContainer).toHaveClass('flex'); + expect(mainContainer).toHaveClass('flex-col'); + expect(mainContainer).toHaveClass('flex-1'); + expect(mainContainer).toHaveClass('items-center'); + expect(mainContainer).toHaveClass('justify-center'); + expect(mainContainer).toHaveClass('bg-zinc-50'); + expect(mainContainer).toHaveClass('font-sans'); + expect(mainContainer).toHaveClass('dark:bg-black'); + }); + + it('should apply correct classes to main element', () => { + const { container } = render(); + const mainElement = container.querySelector('main'); + expect(mainElement).toHaveClass('flex'); + expect(mainElement).toHaveClass('flex-1'); + expect(mainElement).toHaveClass('w-full'); + expect(mainElement).toHaveClass('max-w-3xl'); + expect(mainElement).toHaveClass('flex-col'); + expect(mainElement).toHaveClass('items-center'); + expect(mainElement).toHaveClass('justify-between'); + }); + }); + + describe('Next.js Logo Image', () => { + it('should render the Next.js logo image', () => { + render(); + const nextLogo = screen.getByAltText('Next.js logo'); + expect(nextLogo).toBeInTheDocument(); + }); + + it('should have correct src attribute for Next.js logo', () => { + render(); + const nextLogo = screen.getByAltText('Next.js logo'); + expect(nextLogo).toHaveAttribute('src', '/next.svg'); + }); + + it('should have correct dimensions for Next.js logo', () => { + render(); + const nextLogo = screen.getByAltText('Next.js logo'); + expect(nextLogo).toHaveAttribute('width', '100'); + expect(nextLogo).toHaveAttribute('height', '20'); + }); + + it('should have priority attribute for Next.js logo', () => { + render(); + const nextLogo = screen.getByAltText('Next.js logo'); + expect(nextLogo).toHaveAttribute('priority'); + }); + + it('should have dark:invert class for Next.js logo', () => { + render(); + const nextLogo = screen.getByAltText('Next.js logo'); + expect(nextLogo).toHaveClass('dark:invert'); + }); + }); + + describe('Main Heading', () => { + it('should render the main heading text', () => { + render(); + expect(screen.getByText('To get started, edit the page.tsx file.')).toBeInTheDocument(); + }); + + it('should render heading as h1 element', () => { + render(); + const heading = screen.getByRole('heading', { level: 1 }); + expect(heading).toBeInTheDocument(); + expect(heading).toHaveTextContent('To get started, edit the page.tsx file.'); + }); + + it('should apply correct classes to heading', () => { + render(); + const heading = screen.getByRole('heading', { level: 1 }); + expect(heading).toHaveClass('max-w-xs'); + expect(heading).toHaveClass('text-3xl'); + expect(heading).toHaveClass('font-semibold'); + expect(heading).toHaveClass('leading-10'); + expect(heading).toHaveClass('tracking-tight'); + expect(heading).toHaveClass('text-black'); + expect(heading).toHaveClass('dark:text-zinc-50'); + }); + }); + + describe('Description Paragraph', () => { + it('should render the description paragraph', () => { + render(); + const paragraph = screen.getByText(/Looking for a starting point or more instructions\?/); + expect(paragraph).toBeInTheDocument(); + }); + + it('should apply correct classes to description paragraph', () => { + render(); + const paragraph = screen.getByText(/Looking for a starting point or more instructions\?/); + expect(paragraph).toHaveClass('max-w-md'); + expect(paragraph).toHaveClass('text-lg'); + expect(paragraph).toHaveClass('leading-8'); + expect(paragraph).toHaveClass('text-zinc-600'); + expect(paragraph).toHaveClass('dark:text-zinc-400'); + }); + + it('should contain Templates link text', () => { + render(); + expect(screen.getByText('Templates')).toBeInTheDocument(); + }); + + it('should contain Learning link text', () => { + render(); + expect(screen.getByText('Learning')).toBeInTheDocument(); + }); + }); + + describe('Templates Link', () => { + it('should render Templates link with correct href', () => { + render(); + const templatesLink = screen.getByRole('link', { name: 'Templates' }); + expect(templatesLink).toBeInTheDocument(); + expect(templatesLink).toHaveAttribute( + 'href', + 'https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app' + ); + }); + + it('should apply correct classes to Templates link', () => { + render(); + const templatesLink = screen.getByRole('link', { name: 'Templates' }); + expect(templatesLink).toHaveClass('font-medium'); + expect(templatesLink).toHaveClass('text-zinc-950'); + expect(templatesLink).toHaveClass('dark:text-zinc-50'); + }); + }); + + describe('Learning Link', () => { + it('should render Learning link with correct href', () => { + render(); + const learningLink = screen.getByRole('link', { name: 'Learning' }); + expect(learningLink).toBeInTheDocument(); + expect(learningLink).toHaveAttribute( + 'href', + 'https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app' + ); + }); + + it('should apply correct classes to Learning link', () => { + render(); + const learningLink = screen.getByRole('link', { name: 'Learning' }); + expect(learningLink).toHaveClass('font-medium'); + expect(learningLink).toHaveClass('text-zinc-950'); + expect(learningLink).toHaveClass('dark:text-zinc-50'); + }); + }); + + describe('Vercel Logo Image', () => { + it('should render the Vercel logo image', () => { + render(); + const vercelLogo = screen.getByAltText('Vercel logomark'); + expect(vercelLogo).toBeInTheDocument(); + }); + + it('should have correct src attribute for Vercel logo', () => { + render(); + const vercelLogo = screen.getByAltText('Vercel logomark'); + expect(vercelLogo).toHaveAttribute('src', '/vercel.svg'); + }); + + it('should have correct dimensions for Vercel logo', () => { + render(); + const vercelLogo = screen.getByAltText('Vercel logomark'); + expect(vercelLogo).toHaveAttribute('width', '16'); + expect(vercelLogo).toHaveAttribute('height', '16'); + }); + + it('should have dark:invert class for Vercel logo', () => { + render(); + const vercelLogo = screen.getByAltText('Vercel logomark'); + expect(vercelLogo).toHaveClass('dark:invert'); + }); + }); + + describe('Deploy Now Button/Link', () => { + it('should render Deploy Now link', () => { + render(); + const deployLink = screen.getByRole('link', { name: /Deploy Now/i }); + expect(deployLink).toBeInTheDocument(); + }); + + it('should have correct href for Deploy Now link', () => { + render(); + const deployLink = screen.getByRole('link', { name: /Deploy Now/i }); + expect(deployLink).toHaveAttribute( + 'href', + 'https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app' + ); + }); + + it('should have target="_blank" for Deploy Now link', () => { + render(); + const deployLink = screen.getByRole('link', { name: /Deploy Now/i }); + expect(deployLink).toHaveAttribute('target', '_blank'); + }); + + it('should have rel="noopener noreferrer" for Deploy Now link', () => { + render(); + const deployLink = screen.getByRole('link', { name: /Deploy Now/i }); + expect(deployLink).toHaveAttribute('rel', 'noopener noreferrer'); + }); + + it('should apply correct classes to Deploy Now link', () => { + render(); + const deployLink = screen.getByRole('link', { name: /Deploy Now/i }); + expect(deployLink).toHaveClass('flex'); + expect(deployLink).toHaveClass('h-12'); + expect(deployLink).toHaveClass('w-full'); + expect(deployLink).toHaveClass('items-center'); + expect(deployLink).toHaveClass('justify-center'); + expect(deployLink).toHaveClass('gap-2'); + expect(deployLink).toHaveClass('rounded-full'); + expect(deployLink).toHaveClass('bg-foreground'); + expect(deployLink).toHaveClass('px-5'); + expect(deployLink).toHaveClass('text-background'); + }); + }); + + describe('Documentation Link', () => { + it('should render Documentation link', () => { + render(); + const docLink = screen.getByRole('link', { name: 'Documentation' }); + expect(docLink).toBeInTheDocument(); + }); + + it('should have correct href for Documentation link', () => { + render(); + const docLink = screen.getByRole('link', { name: 'Documentation' }); + expect(docLink).toHaveAttribute( + 'href', + 'https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app' + ); + }); + + it('should have target="_blank" for Documentation link', () => { + render(); + const docLink = screen.getByRole('link', { name: 'Documentation' }); + expect(docLink).toHaveAttribute('target', '_blank'); + }); + + it('should have rel="noopener noreferrer" for Documentation link', () => { + render(); + const docLink = screen.getByRole('link', { name: 'Documentation' }); + expect(docLink).toHaveAttribute('rel', 'noopener noreferrer'); + }); + + it('should apply correct classes to Documentation link', () => { + render(); + const docLink = screen.getByRole('link', { name: 'Documentation' }); + expect(docLink).toHaveClass('flex'); + expect(docLink).toHaveClass('h-12'); + expect(docLink).toHaveClass('w-full'); + expect(docLink).toHaveClass('items-center'); + expect(docLink).toHaveClass('justify-center'); + expect(docLink).toHaveClass('rounded-full'); + expect(docLink).toHaveClass('border'); + expect(docLink).toHaveClass('border-solid'); + }); + }); + + describe('Link Count and Structure', () => { + // Boundary: exact count of links + it('should render exactly 4 anchor links', () => { + render(); + const links = screen.getAllByRole('link'); + expect(links).toHaveLength(4); + }); + + it('should have all external links with security attributes', () => { + render(); + const externalLinks = screen.getAllByRole('link').filter( + link => link.getAttribute('target') === '_blank' + ); + // Deploy Now and Documentation links should have target="_blank" + expect(externalLinks).toHaveLength(2); + + externalLinks.forEach(link => { + expect(link).toHaveAttribute('rel', 'noopener noreferrer'); + }); + }); + }); + + describe('Image Count', () => { + // Boundary: exact count of images + it('should render exactly 2 images', () => { + render(); + const images = screen.getAllByRole('img'); + expect(images).toHaveLength(2); + }); + + it('should have all images with alt text', () => { + render(); + const images = screen.getAllByRole('img'); + images.forEach(img => { + expect(img).toHaveAttribute('alt'); + expect(img.getAttribute('alt')).not.toBe(''); + }); + }); + }); + + describe('Content Container Structure', () => { + it('should render content wrapper with flex and gap classes', () => { + const { container } = render(); + const contentDiv = container.querySelector('.flex.flex-col.items-center.gap-6'); + expect(contentDiv).toBeInTheDocument(); + }); + + it('should render button container with correct classes', () => { + const { container } = render(); + const buttonContainer = container.querySelector('.flex.flex-col.gap-4'); + expect(buttonContainer).toBeInTheDocument(); + }); + }); + + describe('Responsive Classes', () => { + it('should have responsive classes on main element', () => { + const { container } = render(); + const mainElement = container.querySelector('main'); + expect(mainElement?.className).toContain('sm:items-start'); + }); + + it('should have responsive classes on content div', () => { + const { container } = render(); + const contentDiv = container.querySelector('.text-center'); + expect(contentDiv).toBeInTheDocument(); + expect(contentDiv?.className).toContain('sm:text-left'); + expect(contentDiv?.className).toContain('sm:items-start'); + }); + + it('should have responsive classes on button container', () => { + const { container } = render(); + const buttonContainer = container.querySelector('.flex.flex-col.gap-4'); + expect(buttonContainer?.className).toContain('sm:flex-row'); + }); + + it('should have responsive width classes on buttons', () => { + const { container } = render(); + const deployButton = screen.getByRole('link', { name: /Deploy Now/i }); + expect(deployButton.className).toContain('md:w-[158px]'); + }); + }); + + describe('Text Content Verification', () => { + // Equivalence Class: verify all text content is present + it('should contain all expected text content', () => { + render(); + + expect(screen.getByText('To get started, edit the page.tsx file.')).toBeInTheDocument(); + expect(screen.getByText(/Looking for a starting point or more instructions\?/)).toBeInTheDocument(); + expect(screen.getByText('Templates')).toBeInTheDocument(); + expect(screen.getByText('Learning')).toBeInTheDocument(); + expect(screen.getByText('Deploy Now')).toBeInTheDocument(); + expect(screen.getByText('Documentation')).toBeInTheDocument(); + }); + + it('should have text content in correct order', () => { + const { container } = render(); + const text = container.textContent || ''; + + const headingIndex = text.indexOf('To get started'); + const templatesIndex = text.indexOf('Templates'); + const learningIndex = text.indexOf('Learning'); + const deployIndex = text.indexOf('Deploy Now'); + const docsIndex = text.indexOf('Documentation'); + + expect(headingIndex).toBeLessThan(templatesIndex); + expect(templatesIndex).toBeLessThan(learningIndex); + expect(learningIndex).toBeLessThan(deployIndex); + expect(deployIndex).toBeLessThan(docsIndex); + }); + }); + + describe('Layout and Spacing', () => { + it('should have padding classes on main element', () => { + const { container } = render(); + const mainElement = container.querySelector('main'); + expect(mainElement?.className).toContain('py-32'); + expect(mainElement?.className).toContain('px-16'); + }); + + it('should have background color classes', () => { + const { container } = render(); + const mainElement = container.querySelector('main'); + expect(mainElement?.className).toContain('bg-white'); + expect(mainElement?.className).toContain('dark:bg-black'); + }); + }); +});