Skip to content

Commit

Permalink
Merge pull request #22 from brunomachadors/21-automated-playwright-te…
Browse files Browse the repository at this point in the history
…sts-for-portfolio-homepage-and-footer

feat: Initialize locators in HomePage constructor for improved reusab…
  • Loading branch information
brunomachadors authored Jan 5, 2025
2 parents f57395f + c3f82bf commit 7b7aade
Show file tree
Hide file tree
Showing 12 changed files with 251 additions and 48 deletions.
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"lint": "next lint"
},
"dependencies": {
"dotenv": "^16.4.7",
"next": "15.1.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
Expand Down
16 changes: 13 additions & 3 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
import { defineConfig, devices } from '@playwright/test';
import dotenv from 'dotenv';

dotenv.config();

export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
timeout: 30000,
reporter: 'html',

reporter: [['line'], ['github']],
expect: {
timeout: 10_000,
},

use: {
baseURL: 'http://localhost:3000',
baseURL: process.env.BASE_URL || 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},

projects: [
{
name: 'chromium',
Expand Down
18 changes: 7 additions & 11 deletions src/app/components/Button/LinkButton.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,33 @@
import Link from 'next/link';
import { ButtonHTMLAttributes } from 'react';

interface LinkButtonProps {
interface LinkButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
text: string;
href?: string;
type?: 'button' | 'submit' | 'reset'; // Added type prop
onClick?: () => void; // Optional for non-link buttons
extraClasses?: string;
}

export default function LinkButton({
text,
href,
type = 'button',
onClick,
extraClasses = '',
...props
}: LinkButtonProps) {
const baseClasses =
'border border-yellow-500 text-yellow-500 rounded-full px-8 py-4 text-lg transition-transform hover:scale-110';

if (href) {
return (
<Link href={href}>
<button className={`${baseClasses} ${extraClasses}`}>{text}</button>
<button className={`${baseClasses} ${extraClasses}`} {...props}>
{text}
</button>
</Link>
);
}

return (
<button
type={type}
className={`${baseClasses} ${extraClasses}`}
onClick={onClick}
>
<button className={`${baseClasses} ${extraClasses}`} {...props}>
{text}
</button>
);
Expand Down
20 changes: 17 additions & 3 deletions src/app/components/Footer/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
export default function Footer() {
return (
<footer className="w-full flex flex-col items-center justify-start gap-6 p-4 border-t border-white/20 ">
<div className="flex flex-wrap justify-center gap-6">
<footer
className="w-full flex flex-col items-center justify-start gap-6 p-4 border-t border-white/20"
data-test-id="footer-container"
>
<div
className="flex flex-wrap justify-center gap-6"
data-test-id="footer-links"
>
<a
href="https://www.instagram.com/brunomachadors/"
target="_blank"
rel="noopener noreferrer"
aria-label="INSTAGRAM"
className="hover:text-yellow-500 transition transform hover:scale-110 uppercase"
data-test-id="footer-link-instagram"
>
<span className="material-icons text-sm sm:text-xl">instagram</span>
</a>
Expand All @@ -17,6 +24,7 @@ export default function Footer() {
rel="noopener noreferrer"
aria-label="LINKEDIN"
className="hover:text-yellow-500 transition transform hover:scale-110 uppercase"
data-test-id="footer-link-linkedin"
>
<span className="material-icons text-sm sm:text-xl">linkedin</span>
</a>
Expand All @@ -26,6 +34,7 @@ export default function Footer() {
rel="noopener noreferrer"
aria-label="GITHUB"
className="hover:text-yellow-500 transition transform hover:scale-110 uppercase"
data-test-id="footer-link-github"
>
<span className="material-icons text-sm sm:text-xl">github</span>
</a>
Expand All @@ -35,19 +44,24 @@ export default function Footer() {
rel="noopener noreferrer"
aria-label="MEDIUM"
className="hover:text-yellow-500 transition transform hover:scale-110 uppercase"
data-test-id="footer-link-medium"
>
<span className="material-icons text-sm sm:text-xl">Medium</span>
</a>
<a
href="mailto:[email protected]"
aria-label="EMAIL"
className="hover:text-yellow-500 transition transform hover:scale-110 uppercase"
data-test-id="footer-link-email"
>
<span className="material-icons text-sm sm:text-xl">Email</span>
</a>
</div>

<p className="text-sm uppercase text-center px-4">
<p
className="text-sm uppercase text-center px-4"
data-test-id="footer-copyright"
>
© 2024 Bruno Machado.
</p>
</footer>
Expand Down
32 changes: 26 additions & 6 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,36 @@ import LinkButton from './components/Button/LinkButton';

export default function Home() {
return (
<div className="flex items-center justify-center min-h-[85vh] p-8">
<div className="text-center sm:text-center max-w-5xl mx-auto">
<h1 className="text-5xl sm:text-6xl font-bold text-yellow-500 mb-6">
<main
className="flex items-center justify-center min-h-[85vh] p-8"
role="main"
aria-labelledby="page-title"
>
<div
className="text-center sm:text-center max-w-5xl mx-auto"
data-test-id="home-container"
>
<h1
id="page-title"
className="text-5xl sm:text-6xl font-bold text-yellow-500 mb-6"
data-test-id="home-title"
>
Welcome to my Portfolio
</h1>
<p className="text-lg sm:text-2xl mb-8">
<p
className="text-lg sm:text-2xl mb-8"
data-test-id="home-description"
aria-label="Introduction to portfolio content"
>
Explore my skills, experience, and projects as a QA Engineer.
</p>
<LinkButton text="Start here" href="/about" />
<LinkButton
text="Start here"
href="/about"
data-test-id="home-start-button"
aria-label="Navigate to About page"
/>
</div>
</div>
</main>
);
}
24 changes: 24 additions & 0 deletions tests/data/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Home Page Data
export const HOME_DATA = {
title: 'Welcome to my Portfolio',
subtitle: 'Explore my skills, experience, and projects as a QA Engineer.',
startButtonText: 'Start here',
};

// Footer Data
export const FOOTER_DATA = {
links: ['instagram', 'linkedin', 'github', 'medium', 'email'],
};

// Header Data
export const HEADER_DATA = {
menuOptions: [
'home',
'about',
'resume',
'skills',
'projects',
'posts',
'contacts',
],
};
27 changes: 27 additions & 0 deletions tests/footer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { test } from '@playwright/test';
import { FooterPage } from './pages/FooterPage';
import { FOOTER_DATA } from './data';

test.describe('Footer', () => {
test('Links and Visibility', async ({ page }) => {
const footerPage = new FooterPage(page);

await test.step('Go Home', async () => {
await page.goto('/');
});

await test.step('Footer Visible', async () => {
await footerPage.validateFooterVisible();
});

for (const link of FOOTER_DATA.links) {
await test.step(`Check ${link}`, async () => {
await footerPage.validateLinkVisible(link);
});
}

await test.step('Copyright', async () => {
await footerPage.validateCopyrightText('© 2024 Bruno Machado.');
});
});
});
24 changes: 24 additions & 0 deletions tests/home.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { test } from '@playwright/test';
import { HomePage } from './pages/HomePage';

test.describe('Home', () => {
test('Content Validation', async ({ page }) => {
const homePage = new HomePage(page);

await test.step('Go Home', async () => {
await homePage.navigateToHome();
});

await test.step('Check Title', async () => {
await homePage.validateTitleVisible();
});

await test.step('Check Subtitle', async () => {
await homePage.validateSubtitleVisible();
});

await test.step('Check Start Button', async () => {
await homePage.validateStartButtonVisible();
});
});
});
55 changes: 30 additions & 25 deletions tests/menu.spec.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,42 @@
import { test } from '@playwright/test';
import { MenuPage } from './pages/MenuPage';
import { HEADER_DATA } from './data';

const menuOptions = [
'home',
'about',
'resume',
'skills',
'projects',
'posts',
'contacts',
];

test.describe('Menu Navigation', () => {
test('Validate menu navigation for desktop and mobile', async ({
page,
isMobile,
}) => {
test.describe('Menu', () => {
test('Navigation Validation', async ({ page, isMobile }) => {
const menuPage = new MenuPage(page);
await menuPage.navigateToHome();

const mobileMenuOptions = menuOptions.slice(1);
await test.step('Go Home', async () => {
await menuPage.navigateToHome();
});

const mobileMenuOptions = HEADER_DATA.menuOptions.slice(1);

if (isMobile) {
for (const option of mobileMenuOptions) {
await menuPage.openMobileMenu();
await menuPage.validateMobileMenuItemVisible(option);
await menuPage.clickMobileMenuItem(option);
await menuPage.validateURL(option);
await test.step(`Open Mobile: ${option}`, async () => {
await menuPage.openMobileMenu();
});

await test.step(`Check Mobile Item: ${option}`, async () => {
await menuPage.validateMobileMenuItemVisible(option);
});

await test.step(`Click Mobile Item: ${option}`, async () => {
await menuPage.clickMobileMenuItem(option);
await menuPage.validateURL(option);
});
}
} else {
for (const option of menuOptions) {
await menuPage.validateMenuItemVisible(option);
await menuPage.clickMenuItem(option);
await menuPage.validateURL(option);
for (const option of HEADER_DATA.menuOptions) {
await test.step(`Check Item: ${option}`, async () => {
await menuPage.validateMenuItemVisible(option);
});

await test.step(`Click Item: ${option}`, async () => {
await menuPage.clickMenuItem(option);
await menuPage.validateURL(option);
});
}
}
});
Expand Down
32 changes: 32 additions & 0 deletions tests/pages/FooterPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Page, Locator, expect } from '@playwright/test';

export class FooterPage {
readonly page: Page;
readonly footerContainer: Locator;
readonly footerLinks: Locator;
readonly copyrightText: Locator;

constructor(page: Page) {
this.page = page;
this.footerContainer = page.locator('[data-test-id="footer-container"]');
this.footerLinks = page.locator('[data-test-id="footer-links"] a');
this.copyrightText = page.locator('[data-test-id="footer-copyright"]');
}

async validateFooterVisible() {
await expect(this.footerContainer).toBeVisible();
}

async validateLinkVisible(linkTestId: string) {
const link = this.page.locator(
`[data-test-id="footer-link-${linkTestId}"]`
);
await expect(link).toBeVisible();
await expect(link).toHaveAttribute('href', expect.stringContaining('http'));
}

async validateCopyrightText(expectedText: string) {
await expect(this.copyrightText).toBeVisible();
await expect(this.copyrightText).toHaveText(expectedText);
}
}
Loading

0 comments on commit 7b7aade

Please sign in to comment.