+
/
{currentLabel}
From 11790ae3c8284ba142488e1e47ea1c745c27e54a Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 28 Aug 2026 17:18:45 +0800
Subject: [PATCH 08/40] refactor(blog): update article page Read more articles
link to use ArrowRight icon from Lucide React
---
src/app/blog/[slug]/page.tsx | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/app/blog/[slug]/page.tsx b/src/app/blog/[slug]/page.tsx
index 11347d3..1d90987 100644
--- a/src/app/blog/[slug]/page.tsx
+++ b/src/app/blog/[slug]/page.tsx
@@ -3,6 +3,7 @@ import { BreadcrumbItem } from '@/types/breadcrumb-item';
import ArticleHeader from '@/components/sections/article-header';
import Footer from '@/components/sections/footer';
import Link from 'next/link';
+import { ArrowRight } from 'lucide-react';
type ArticlePageProps = {
params: Promise<{ slug: string }>;
@@ -47,9 +48,10 @@ export default async function ArticlePage({ params }: ArticlePageProps) {
- Read more articles →
+
Read more articles
+
From bcbcc89abced5ad10492b9397284359fd3b27db6 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 28 Aug 2026 17:21:27 +0800
Subject: [PATCH 09/40] refactor(blog): remove color transitions to Read more
links
---
src/app/blog/page.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/app/blog/page.tsx b/src/app/blog/page.tsx
index 71e747a..0d2e413 100644
--- a/src/app/blog/page.tsx
+++ b/src/app/blog/page.tsx
@@ -98,7 +98,7 @@ export default function BlogPage() {
From 716c9b8f0a3e3aad777aeb8165cefd16dd99e155 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 28 Aug 2026 17:33:37 +0800
Subject: [PATCH 10/40] refactor(blog): make Read more links only has the first
letter in uppercase
---
src/app/blog/page.tsx | 2 +-
src/components/sections/blog.tsx | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/app/blog/page.tsx b/src/app/blog/page.tsx
index 0d2e413..ede2b9b 100644
--- a/src/app/blog/page.tsx
+++ b/src/app/blog/page.tsx
@@ -102,7 +102,7 @@ export default function BlogPage() {
href={blogUrl}
aria-label={`Read more about ${title}`}
>
- Read More
+ Read more
diff --git a/src/components/sections/blog.tsx b/src/components/sections/blog.tsx
index 924a7c2..e52c327 100644
--- a/src/components/sections/blog.tsx
+++ b/src/components/sections/blog.tsx
@@ -55,7 +55,7 @@ const Blog = () => {
href={blogUrl}
aria-label={`Read more about ${title}`}
>
- Read More
+ Read more
From d443711c0b32604c2525b8c6c9456c3c16a9eeec Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 28 Aug 2026 17:44:32 +0800
Subject: [PATCH 11/40] test(breadcrumb): renders the Home link
---
src/components/sections/breadcrumb.test.tsx | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
create mode 100644 src/components/sections/breadcrumb.test.tsx
diff --git a/src/components/sections/breadcrumb.test.tsx b/src/components/sections/breadcrumb.test.tsx
new file mode 100644
index 0000000..6e1d341
--- /dev/null
+++ b/src/components/sections/breadcrumb.test.tsx
@@ -0,0 +1,16 @@
+import { render, screen } from '@testing-library/react';
+import Breadcrumb from './breadcrumb';
+import type { BreadcrumbItem } from '@/types/breadcrumb-item';
+
+describe('Breadcrumb', () => {
+ const items: BreadcrumbItem[] = [
+ { label: 'Blog', href: '/blog' },
+ { label: 'Technology', href: '/blog/technology' },
+ ];
+
+ test('renders the Home link', () => {
+ render( );
+ const link = screen.getByRole('link', { name: 'Home' });
+ expect(link).toHaveAttribute('href', '/');
+ });
+});
From e3de2894a10b0c52b027b01708325b6283f2fa00 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 28 Aug 2026 17:55:33 +0800
Subject: [PATCH 12/40] test(breadcrumb): renders all succeeding breadcrumb
item links
---
src/components/sections/breadcrumb.test.tsx | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/components/sections/breadcrumb.test.tsx b/src/components/sections/breadcrumb.test.tsx
index 6e1d341..5e45750 100644
--- a/src/components/sections/breadcrumb.test.tsx
+++ b/src/components/sections/breadcrumb.test.tsx
@@ -13,4 +13,11 @@ describe('Breadcrumb', () => {
const link = screen.getByRole('link', { name: 'Home' });
expect(link).toHaveAttribute('href', '/');
});
+ test('renders all succeeding breadcrumb item links', () => {
+ render( );
+ items.forEach(({ label, href }) => {
+ const link = screen.getByRole('link', { name: label });
+ expect(link).toHaveAttribute('href', href);
+ });
+ });
});
From 88c78019604bca4432f42e61318b648aa76387b7 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 28 Aug 2026 17:59:01 +0800
Subject: [PATCH 13/40] test(breadcrumb): renders an accessible breadcrumb
navigation
---
src/components/sections/breadcrumb.test.tsx | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/src/components/sections/breadcrumb.test.tsx b/src/components/sections/breadcrumb.test.tsx
index 5e45750..f024b20 100644
--- a/src/components/sections/breadcrumb.test.tsx
+++ b/src/components/sections/breadcrumb.test.tsx
@@ -20,4 +20,9 @@ describe('Breadcrumb', () => {
expect(link).toHaveAttribute('href', href);
});
});
+ test('renders an accessible breadcrumb navigation', () => {
+ render( );
+ const nav = screen.getByRole('navigation', { name: /breadcrumb/i });
+ expect(nav).toBeInTheDocument();
+ });
});
From 3bd09629092765d3cba2c7441e98f867eb910d17 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 28 Aug 2026 18:01:49 +0800
Subject: [PATCH 14/40] test(breadcrumb): renders the current page with
aria-current
---
src/components/sections/breadcrumb.test.tsx | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/src/components/sections/breadcrumb.test.tsx b/src/components/sections/breadcrumb.test.tsx
index f024b20..e63c488 100644
--- a/src/components/sections/breadcrumb.test.tsx
+++ b/src/components/sections/breadcrumb.test.tsx
@@ -7,7 +7,6 @@ describe('Breadcrumb', () => {
{ label: 'Blog', href: '/blog' },
{ label: 'Technology', href: '/blog/technology' },
];
-
test('renders the Home link', () => {
render( );
const link = screen.getByRole('link', { name: 'Home' });
@@ -25,4 +24,9 @@ describe('Breadcrumb', () => {
const nav = screen.getByRole('navigation', { name: /breadcrumb/i });
expect(nav).toBeInTheDocument();
});
+ test('renders the current page with aria-current', () => {
+ render( );
+ const currentPage = screen.getByText('Current Article');
+ expect(currentPage).toHaveAttribute('aria-current', 'page');
+ });
});
From 2e567da6c31bc7ee8ec506c79b5bc1a1d00552bc Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 28 Aug 2026 18:05:01 +0800
Subject: [PATCH 15/40] test(breadcrumb): renders without succeeding breadcrumb
item links
---
src/components/sections/breadcrumb.test.tsx | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/src/components/sections/breadcrumb.test.tsx b/src/components/sections/breadcrumb.test.tsx
index e63c488..f9940c9 100644
--- a/src/components/sections/breadcrumb.test.tsx
+++ b/src/components/sections/breadcrumb.test.tsx
@@ -9,7 +9,7 @@ describe('Breadcrumb', () => {
];
test('renders the Home link', () => {
render( );
- const link = screen.getByRole('link', { name: 'Home' });
+ const link = screen.getByRole('link', { name: /home/i });
expect(link).toHaveAttribute('href', '/');
});
test('renders all succeeding breadcrumb item links', () => {
@@ -26,7 +26,14 @@ describe('Breadcrumb', () => {
});
test('renders the current page with aria-current', () => {
render( );
- const currentPage = screen.getByText('Current Article');
+ const currentPage = screen.getByText(/current article/i);
expect(currentPage).toHaveAttribute('aria-current', 'page');
});
+ test('renders without succeeding breadcrumb item links', () => {
+ render( );
+ const homeLink = screen.getByRole('link', { name: /home/i });
+ const currentPage = screen.getByText(/current article/i);
+ expect(homeLink).toBeInTheDocument();
+ expect(currentPage).toBeInTheDocument();
+ });
});
From 6fd9355c89834bd2f288d954df6643b4ceea94a1 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 28 Aug 2026 18:21:52 +0800
Subject: [PATCH 16/40] test(article-header): renders the article title
---
.../sections/article-header.test.tsx | 28 +++++++++++++++++++
1 file changed, 28 insertions(+)
create mode 100644 src/components/sections/article-header.test.tsx
diff --git a/src/components/sections/article-header.test.tsx b/src/components/sections/article-header.test.tsx
new file mode 100644
index 0000000..77a893b
--- /dev/null
+++ b/src/components/sections/article-header.test.tsx
@@ -0,0 +1,28 @@
+import { render, screen } from '@testing-library/react';
+import ArticleHeader from './article-header';
+import { BreadcrumbItem } from '@/types/breadcrumb-item';
+import { getSlug } from '@/lib/utils';
+
+describe('Article Header', () => {
+ const breadcrumbItems: BreadcrumbItem[] = [{ label: 'Blog', href: '/blog' }];
+ const blogMeta = {
+ id: 4,
+ title: 'How I Contributed to a Laravel Application Without Knowing Laravel',
+ description:
+ 'Lessons from contributing to a Laravel application, where I discovered that recognizing familiar patterns is often more valuable than starting over.',
+ publishDate: 'August 2026',
+ readTime: '4 min read',
+ tags: ['#laravel', '#learning', '#webdev'],
+ blogUrl: `/blog/${getSlug('How I Contributed to a Laravel Application Without Knowing Laravel')}`,
+ imgUrl: '/img/blog-4.jpg',
+ imgAlt: 'Keyboard caps laid down on a white table.',
+ };
+
+ test('renders the article title', () => {
+ render(
+ ,
+ );
+ const title = screen.getByRole('heading', { name: blogMeta.title });
+ expect(title).toBeInTheDocument();
+ });
+});
From 8eabde8ebd0839f8f39a49c42612132431370437 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 28 Aug 2026 18:23:58 +0800
Subject: [PATCH 17/40] test(article-header): renders the article publish date
---
src/components/sections/article-header.test.tsx | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/components/sections/article-header.test.tsx b/src/components/sections/article-header.test.tsx
index 77a893b..a935dc7 100644
--- a/src/components/sections/article-header.test.tsx
+++ b/src/components/sections/article-header.test.tsx
@@ -25,4 +25,11 @@ describe('Article Header', () => {
const title = screen.getByRole('heading', { name: blogMeta.title });
expect(title).toBeInTheDocument();
});
+ test('renders the article publish date', () => {
+ render(
+ ,
+ );
+ const publishDate = screen.getByText(blogMeta.publishDate);
+ expect(publishDate).toBeInTheDocument();
+ });
});
From 4388085292e454481e744adfbab138dbeaf4f57c Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 28 Aug 2026 18:32:05 +0800
Subject: [PATCH 18/40] perf(blog): add loading eager attribute to author
profile picture
---
src/components/sections/article-header.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/components/sections/article-header.tsx b/src/components/sections/article-header.tsx
index d773eed..0cfd62b 100644
--- a/src/components/sections/article-header.tsx
+++ b/src/components/sections/article-header.tsx
@@ -67,6 +67,7 @@ const ArticleHeader = ({ breadcrumbItems, blogMeta }: ArticleHeaderProps) => {
width={683}
height={771}
className="object-cover"
+ loading="eager"
/>
From b214cf9274f2633e1bafb875316b615ed0d12497 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 28 Aug 2026 18:34:58 +0800
Subject: [PATCH 19/40] test(article-header): renders the article author
---
src/components/sections/article-header.test.tsx | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/components/sections/article-header.test.tsx b/src/components/sections/article-header.test.tsx
index a935dc7..9e767e5 100644
--- a/src/components/sections/article-header.test.tsx
+++ b/src/components/sections/article-header.test.tsx
@@ -32,4 +32,11 @@ describe('Article Header', () => {
const publishDate = screen.getByText(blogMeta.publishDate);
expect(publishDate).toBeInTheDocument();
});
+ test('renders the article author', () => {
+ render(
+ ,
+ );
+ const authorName = screen.getByText('Elmar Chavez');
+ expect(authorName).toBeInTheDocument();
+ });
});
From a3a0a8f27fca17b2799321329676392d7719360f Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 28 Aug 2026 18:36:30 +0800
Subject: [PATCH 20/40] test(article-header): renders the read time
---
src/components/sections/article-header.test.tsx | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/components/sections/article-header.test.tsx b/src/components/sections/article-header.test.tsx
index 9e767e5..ddad04f 100644
--- a/src/components/sections/article-header.test.tsx
+++ b/src/components/sections/article-header.test.tsx
@@ -39,4 +39,11 @@ describe('Article Header', () => {
const authorName = screen.getByText('Elmar Chavez');
expect(authorName).toBeInTheDocument();
});
+ test('renders the read time', () => {
+ render(
+ ,
+ );
+ const readTime = screen.getByText(blogMeta.readTime);
+ expect(readTime).toBeInTheDocument();
+ });
});
From f7fe840b6926ba548d96bfbf6d874848ee44801c Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 28 Aug 2026 18:38:37 +0800
Subject: [PATCH 21/40] test(article-header): renders all the article tags
---
src/components/sections/article-header.test.tsx | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/src/components/sections/article-header.test.tsx b/src/components/sections/article-header.test.tsx
index ddad04f..c84ed92 100644
--- a/src/components/sections/article-header.test.tsx
+++ b/src/components/sections/article-header.test.tsx
@@ -46,4 +46,13 @@ describe('Article Header', () => {
const readTime = screen.getByText(blogMeta.readTime);
expect(readTime).toBeInTheDocument();
});
+ test('renders all the article tags', () => {
+ render(
+ ,
+ );
+ blogMeta.tags.forEach((tag) => {
+ const text = screen.getByText(tag);
+ expect(text).toBeInTheDocument();
+ });
+ });
});
From 1feb2a834bb5a81e359a0c1c3eea642f029bbe3e Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 28 Aug 2026 18:48:56 +0800
Subject: [PATCH 22/40] test(article-header): renders the banner image with alt
text
---
src/components/sections/article-header.test.tsx | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/components/sections/article-header.test.tsx b/src/components/sections/article-header.test.tsx
index c84ed92..d2158ed 100644
--- a/src/components/sections/article-header.test.tsx
+++ b/src/components/sections/article-header.test.tsx
@@ -55,4 +55,11 @@ describe('Article Header', () => {
expect(text).toBeInTheDocument();
});
});
+ test('renders the banner image with alt text', () => {
+ render(
+ ,
+ );
+ const bannerImage = screen.getByRole('img', { name: blogMeta.imgAlt });
+ expect(bannerImage).toBeInTheDocument();
+ });
});
From 8f02c75d01a5ae376c2932db23b971d1b306aa59 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 28 Aug 2026 19:06:03 +0800
Subject: [PATCH 23/40] test(article-header): renders the blog not found
variant
---
.../sections/article-header.test.tsx | 24 +++++++++++++++++++
1 file changed, 24 insertions(+)
diff --git a/src/components/sections/article-header.test.tsx b/src/components/sections/article-header.test.tsx
index d2158ed..0ce729c 100644
--- a/src/components/sections/article-header.test.tsx
+++ b/src/components/sections/article-header.test.tsx
@@ -62,4 +62,28 @@ describe('Article Header', () => {
const bannerImage = screen.getByRole('img', { name: blogMeta.imgAlt });
expect(bannerImage).toBeInTheDocument();
});
+ test('renders the blog not found variant', () => {
+ render(
+ ,
+ );
+ const title = screen.getByRole('heading', { name: /blog not found/i });
+ expect(title).toBeInTheDocument();
+ const description = screen.getByText(
+ /^seems like this file doesn't exist/i,
+ );
+ expect(description).toBeInTheDocument();
+ });
});
From 366cc41ae06c8b751d75c55356aa8a3eeadcb39b Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 28 Aug 2026 19:09:39 +0800
Subject: [PATCH 24/40] test(article-header): renders the breadcrumb component
---
src/components/sections/article-header.test.tsx | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/components/sections/article-header.test.tsx b/src/components/sections/article-header.test.tsx
index 0ce729c..625ef37 100644
--- a/src/components/sections/article-header.test.tsx
+++ b/src/components/sections/article-header.test.tsx
@@ -86,4 +86,11 @@ describe('Article Header', () => {
);
expect(description).toBeInTheDocument();
});
+ test('renders the breadcrumb component', () => {
+ render(
+ ,
+ );
+ const breadcrumb = screen.getByRole('navigation', { name: /breadcrumb/i });
+ expect(breadcrumb).toBeInTheDocument();
+ });
});
From a951fc77760c3c05b3aadb0ec2649c76c160758e Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 28 Aug 2026 19:11:26 +0800
Subject: [PATCH 25/40] test(article-header): renders the theme toggle
component
---
src/components/sections/article-header.test.tsx | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/components/sections/article-header.test.tsx b/src/components/sections/article-header.test.tsx
index 625ef37..6e193e3 100644
--- a/src/components/sections/article-header.test.tsx
+++ b/src/components/sections/article-header.test.tsx
@@ -93,4 +93,11 @@ describe('Article Header', () => {
const breadcrumb = screen.getByRole('navigation', { name: /breadcrumb/i });
expect(breadcrumb).toBeInTheDocument();
});
+ test('renders the theme toggle component', () => {
+ render(
+ ,
+ );
+ const themeToggle = screen.getByRole('button', { name: /toggle theme/i });
+ expect(themeToggle).toBeInTheDocument();
+ });
});
From 4cf67104d2a83b7923ba95748e1ee6a313d92a45 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 28 Aug 2026 19:31:14 +0800
Subject: [PATCH 26/40] test(e2e): visitor can view the Blog page
---
tests/blog.spec.ts | 29 +++++++++++++++++++++--------
1 file changed, 21 insertions(+), 8 deletions(-)
diff --git a/tests/blog.spec.ts b/tests/blog.spec.ts
index 48ef01a..c3abbb0 100644
--- a/tests/blog.spec.ts
+++ b/tests/blog.spec.ts
@@ -1,12 +1,25 @@
import { test, expect } from '@playwright/test';
import { blogList } from '@/data/blog-list';
-test('blog article link has correct href', async ({ page }) => {
- await page.goto('/');
- const blog = blogList[0];
- expect(blog).toBeDefined();
- const link = page.getByRole('link', {
- name: `Read more about ${blog.title}`,
- });
- await expect(link).toHaveAttribute('href', blog.blogUrl);
+test.describe('Blog', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/');
+ });
+ test('visitor can see the Blog section', async ({ page }) => {
+ const heading = page.getByRole('heading', { name: /^blog$/i });
+ await expect(heading).toBeVisible();
+ });
+ test('blog article link has correct href', async ({ page }) => {
+ const blog = blogList[0];
+ expect(blog).toBeDefined();
+ const link = page.getByRole('link', {
+ name: `Read more about ${blog.title}`,
+ });
+ await expect(link).toHaveAttribute('href', blog.blogUrl);
+ });
+ test('visitor can view the Blog page', async ({ page }) => {
+ await page.goto('/blog');
+ const heading = page.getByRole('heading', { name: /^blog$/i });
+ await expect(heading).toBeVisible();
+ });
});
From c7d80ff3295a67c778be88b258d1ae812b687b8f Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 28 Aug 2026 19:50:46 +0800
Subject: [PATCH 27/40] test(e2e): visitor can view a blog article
---
tests/blog.spec.ts | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/tests/blog.spec.ts b/tests/blog.spec.ts
index c3abbb0..b6f6cea 100644
--- a/tests/blog.spec.ts
+++ b/tests/blog.spec.ts
@@ -22,4 +22,11 @@ test.describe('Blog', () => {
const heading = page.getByRole('heading', { name: /^blog$/i });
await expect(heading).toBeVisible();
});
+ test('visitor can view a blog article', async ({ page }) => {
+ const blog = blogList[0];
+ expect(blog).toBeDefined();
+ await page.goto(blog.blogUrl);
+ const blogTitle = page.getByRole('heading', { name: blog.title });
+ await expect(blogTitle).toBeVisible();
+ });
});
From 8e63d22940e3cdd238ca32bd3599d0111ca1bd8c Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 28 Aug 2026 19:59:04 +0800
Subject: [PATCH 28/40] test(e2e): visitor can see a blog article item on the
Blog page
---
tests/blog.spec.ts | 16 +++++++++++++---
1 file changed, 13 insertions(+), 3 deletions(-)
diff --git a/tests/blog.spec.ts b/tests/blog.spec.ts
index b6f6cea..58a6043 100644
--- a/tests/blog.spec.ts
+++ b/tests/blog.spec.ts
@@ -2,14 +2,13 @@ import { test, expect } from '@playwright/test';
import { blogList } from '@/data/blog-list';
test.describe('Blog', () => {
- test.beforeEach(async ({ page }) => {
- await page.goto('/');
- });
test('visitor can see the Blog section', async ({ page }) => {
+ await page.goto('/');
const heading = page.getByRole('heading', { name: /^blog$/i });
await expect(heading).toBeVisible();
});
test('blog article link has correct href', async ({ page }) => {
+ await page.goto('/');
const blog = blogList[0];
expect(blog).toBeDefined();
const link = page.getByRole('link', {
@@ -29,4 +28,15 @@ test.describe('Blog', () => {
const blogTitle = page.getByRole('heading', { name: blog.title });
await expect(blogTitle).toBeVisible();
});
+ test('visitor can see a blog article item on the Blog page', async ({
+ page,
+ }) => {
+ const blog = blogList[0];
+ expect(blog).toBeDefined();
+ await page.goto('/blog');
+ const heading = page.getByRole('heading', { name: blog.title });
+ const description = page.getByText(blog.description);
+ await expect(heading).toBeVisible();
+ await expect(description).toBeVisible();
+ });
});
From 7029e05cb6afa5e90a256bf6c65302ed02d60908 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 28 Aug 2026 20:13:13 +0800
Subject: [PATCH 29/40] test(e2e): visitor sees the Blog not found page for an
invalid article
---
tests/blog.spec.ts | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/tests/blog.spec.ts b/tests/blog.spec.ts
index 58a6043..b20b605 100644
--- a/tests/blog.spec.ts
+++ b/tests/blog.spec.ts
@@ -39,4 +39,13 @@ test.describe('Blog', () => {
await expect(heading).toBeVisible();
await expect(description).toBeVisible();
});
+ test('visitor sees the Blog not found page for an invalid article', async ({
+ page,
+ }) => {
+ await page.goto('/blog/this-article-does-not-exist');
+ const heading = page.getByRole('heading', { name: /^blog not found$/i });
+ const description = page.getByText(/^seems like this file doesn't exist/i);
+ await expect(heading).toBeVisible();
+ await expect(description).toBeVisible();
+ });
});
From b607485f68e14ec396cf2472e9e284bea084be43 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 28 Aug 2026 20:27:23 +0800
Subject: [PATCH 30/40] test(utils): generates a URL slug from a title
---
src/lib/utils.test.ts | 12 ++++++++++++
1 file changed, 12 insertions(+)
create mode 100644 src/lib/utils.test.ts
diff --git a/src/lib/utils.test.ts b/src/lib/utils.test.ts
new file mode 100644
index 0000000..8f05f0c
--- /dev/null
+++ b/src/lib/utils.test.ts
@@ -0,0 +1,12 @@
+import { getSlug } from './utils';
+
+describe('Utility functions', () => {
+ test('generates a URL slug from a title', () => {
+ const result = getSlug(
+ 'How I Contributed to a Laravel Application Without Knowing Laravel',
+ );
+ expect(result).toBe(
+ 'how-i-contributed-to-a-laravel-application-without-knowing-laravel',
+ );
+ });
+});
From 7e2c8ac47f7edb75c837c74acbd23a09d9d876fc Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 28 Aug 2026 21:13:39 +0800
Subject: [PATCH 31/40] test(utils): normalizes capitalization and whitespace
for blog titles
---
src/lib/utils.test.ts | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/src/lib/utils.test.ts b/src/lib/utils.test.ts
index 8f05f0c..8628530 100644
--- a/src/lib/utils.test.ts
+++ b/src/lib/utils.test.ts
@@ -9,4 +9,13 @@ describe('Utility functions', () => {
'how-i-contributed-to-a-laravel-application-without-knowing-laravel',
);
});
+ test('normalizes capitalization and whitespace', () => {
+ expect(
+ getSlug(
+ ' How I Contributed to a Laravel Application Without Knowing Laravel ',
+ ),
+ ).toBe(
+ 'how-i-contributed-to-a-laravel-application-without-knowing-laravel',
+ );
+ });
});
From 75e525e17ce9f1c61f6f877453d80687dc8c527b Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 28 Aug 2026 21:17:18 +0800
Subject: [PATCH 32/40] test(utils): removes punctuation from the blog title
---
src/lib/utils.test.ts | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/src/lib/utils.test.ts b/src/lib/utils.test.ts
index 8628530..af8e71a 100644
--- a/src/lib/utils.test.ts
+++ b/src/lib/utils.test.ts
@@ -1,7 +1,7 @@
import { getSlug } from './utils';
describe('Utility functions', () => {
- test('generates a URL slug from a title', () => {
+ test('generates a URL slug from the blog title', () => {
const result = getSlug(
'How I Contributed to a Laravel Application Without Knowing Laravel',
);
@@ -9,7 +9,7 @@ describe('Utility functions', () => {
'how-i-contributed-to-a-laravel-application-without-knowing-laravel',
);
});
- test('normalizes capitalization and whitespace', () => {
+ test('normalizes capitalization and whitespace from the blog title', () => {
expect(
getSlug(
' How I Contributed to a Laravel Application Without Knowing Laravel ',
@@ -18,4 +18,11 @@ describe('Utility functions', () => {
'how-i-contributed-to-a-laravel-application-without-knowing-laravel',
);
});
+ test('removes punctuation from the blog title', () => {
+ expect(
+ getSlug(
+ 'Why Every Software Engineer Should Read "The Psychology of Money"',
+ ),
+ ).toBe('why-every-software-engineer-should-read-the-psychology-of-money');
+ });
});
From 108d067ce6e4ce95cfd994df4bb9734f25a4e537 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 28 Aug 2026 21:26:26 +0800
Subject: [PATCH 33/40] test(utils): returns blog metadata for a valid slug
---
src/lib/utils.test.ts | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/src/lib/utils.test.ts b/src/lib/utils.test.ts
index af8e71a..9473c1f 100644
--- a/src/lib/utils.test.ts
+++ b/src/lib/utils.test.ts
@@ -1,4 +1,5 @@
-import { getSlug } from './utils';
+import { getBlogMeta, getSlug } from './utils';
+import { BLOGS } from '@/data/blog-list';
describe('Utility functions', () => {
test('generates a URL slug from the blog title', () => {
@@ -25,4 +26,11 @@ describe('Utility functions', () => {
),
).toBe('why-every-software-engineer-should-read-the-psychology-of-money');
});
+ test('returns blog metadata for a valid slug', () => {
+ const blogTitle =
+ 'How I Contributed to a Laravel Application Without Knowing Laravel';
+ const blogSlug = getSlug(blogTitle);
+ const blogMeta = BLOGS.find(({ title }) => title === blogTitle);
+ expect(blogMeta).toEqual(getBlogMeta(blogSlug));
+ });
});
From 2d287e43ad8239b0d1b9f4ad9a3b9ee5500beac6 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 28 Aug 2026 21:31:50 +0800
Subject: [PATCH 34/40] test(utils): returns the fallback not found blog
metadata for an invalid slug
---
src/lib/utils.test.ts | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/src/lib/utils.test.ts b/src/lib/utils.test.ts
index 9473c1f..d7aa603 100644
--- a/src/lib/utils.test.ts
+++ b/src/lib/utils.test.ts
@@ -33,4 +33,10 @@ describe('Utility functions', () => {
const blogMeta = BLOGS.find(({ title }) => title === blogTitle);
expect(blogMeta).toEqual(getBlogMeta(blogSlug));
});
+ test('returns the fallback not found blog metadata for an invalid slug', () => {
+ const blogSlug = 'this-is-an-invalid-article-slug';
+ const blogMeta = getBlogMeta(blogSlug);
+ expect(blogMeta.id).toBe(-99);
+ expect(blogMeta.title).toBe('Blog not found');
+ });
});
From 759904897962fb086386e4bd757e98f359814192 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 28 Aug 2026 21:43:11 +0800
Subject: [PATCH 35/40] test(utils): group tests into their function of concern
---
src/lib/utils.test.ts | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/lib/utils.test.ts b/src/lib/utils.test.ts
index d7aa603..f20003a 100644
--- a/src/lib/utils.test.ts
+++ b/src/lib/utils.test.ts
@@ -1,7 +1,7 @@
import { getBlogMeta, getSlug } from './utils';
import { BLOGS } from '@/data/blog-list';
-describe('Utility functions', () => {
+describe('getSlug', () => {
test('generates a URL slug from the blog title', () => {
const result = getSlug(
'How I Contributed to a Laravel Application Without Knowing Laravel',
@@ -26,6 +26,8 @@ describe('Utility functions', () => {
),
).toBe('why-every-software-engineer-should-read-the-psychology-of-money');
});
+});
+describe('getBlogMeta', () => {
test('returns blog metadata for a valid slug', () => {
const blogTitle =
'How I Contributed to a Laravel Application Without Knowing Laravel';
From ae45898dca00b2e880b64ab5a0c025601011b512 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 28 Aug 2026 22:15:08 +0800
Subject: [PATCH 36/40] test(e2e): update visitor can view the Blog page when
they click the view all link
---
tests/blog.spec.ts | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/tests/blog.spec.ts b/tests/blog.spec.ts
index b20b605..748c10b 100644
--- a/tests/blog.spec.ts
+++ b/tests/blog.spec.ts
@@ -17,7 +17,9 @@ test.describe('Blog', () => {
await expect(link).toHaveAttribute('href', blog.blogUrl);
});
test('visitor can view the Blog page', async ({ page }) => {
- await page.goto('/blog');
+ await page.goto('/');
+ const viewAllLink = page.locator('a[href="/blog"]');
+ await viewAllLink.click();
const heading = page.getByRole('heading', { name: /^blog$/i });
await expect(heading).toBeVisible();
});
From 188934e601eb3082966316c14169eb0ae5550134 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 28 Aug 2026 22:23:22 +0800
Subject: [PATCH 37/40] test(e2e): visitor can go back to Home page when
clicking the home link in breadcrumb
---
tests/blog.spec.ts | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/tests/blog.spec.ts b/tests/blog.spec.ts
index 748c10b..dd9100d 100644
--- a/tests/blog.spec.ts
+++ b/tests/blog.spec.ts
@@ -50,4 +50,12 @@ test.describe('Blog', () => {
await expect(heading).toBeVisible();
await expect(description).toBeVisible();
});
+ test('visitor can go back to Home page when clicking the home link in breadcrumb', async ({
+ page,
+ }) => {
+ await page.goto('/blog');
+ const homeLink = page.getByRole('link', { name: /home/i });
+ await homeLink.click();
+ await expect(page).toHaveURL('/');
+ });
});
From 9f90c5d209e5b8ea8fe4a49cb3e5047914c16830 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 28 Aug 2026 22:38:30 +0800
Subject: [PATCH 38/40] test(e2e): visitor can go back to Home page from an
article page
---
tests/blog.spec.ts | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/tests/blog.spec.ts b/tests/blog.spec.ts
index dd9100d..f45305f 100644
--- a/tests/blog.spec.ts
+++ b/tests/blog.spec.ts
@@ -58,4 +58,13 @@ test.describe('Blog', () => {
await homeLink.click();
await expect(page).toHaveURL('/');
});
+ test('visitor can go back to Home page from an article page', async ({
+ page,
+ }) => {
+ const blogMeta = blogList[0];
+ await page.goto(blogMeta.blogUrl);
+ const homeLink = page.getByRole('link', { name: /^home$/i });
+ await homeLink.click();
+ await expect(page).toHaveURL('/');
+ });
});
From 494be3a5fd4d39dfb518f1bba2d334b1c6d83504 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 28 Aug 2026 22:46:11 +0800
Subject: [PATCH 39/40] test(e2e): visitor can go back to Blog page from an
article page
---
tests/blog.spec.ts | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/tests/blog.spec.ts b/tests/blog.spec.ts
index f45305f..9957c13 100644
--- a/tests/blog.spec.ts
+++ b/tests/blog.spec.ts
@@ -67,4 +67,13 @@ test.describe('Blog', () => {
await homeLink.click();
await expect(page).toHaveURL('/');
});
+ test('visitor can go back to Blog page from an article page', async ({
+ page,
+ }) => {
+ const blogMeta = blogList[0];
+ await page.goto(blogMeta.blogUrl);
+ const blogLink = page.getByRole('link', { name: /^blog$/i });
+ await blogLink.click();
+ await expect(page).toHaveURL('/blog');
+ });
});
From 339aa6e8a475988881e3352094391e1021fc0383 Mon Sep 17 00:00:00 2001
From: Elmar Chavez
Date: Fri, 28 Aug 2026 22:55:25 +0800
Subject: [PATCH 40/40] test(e2e): visitor can go back to Blog page from an
article page using Read more articles link
---
tests/blog.spec.ts | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/tests/blog.spec.ts b/tests/blog.spec.ts
index 9957c13..203c0d4 100644
--- a/tests/blog.spec.ts
+++ b/tests/blog.spec.ts
@@ -76,4 +76,13 @@ test.describe('Blog', () => {
await blogLink.click();
await expect(page).toHaveURL('/blog');
});
+ test('visitor can go back to Blog page from an article page using Read more articles link', async ({
+ page,
+ }) => {
+ const blogMeta = blogList[0];
+ await page.goto(blogMeta.blogUrl);
+ const blogLink = page.getByRole('link', { name: /read more articles/i });
+ await blogLink.click();
+ await expect(page).toHaveURL('/blog');
+ });
});