Skip to content

Commit fc08ddc

Browse files
committed
feat(blog): integrate ShareModal into blog post page
1 parent ddbc625 commit fc08ddc

File tree

6 files changed

+166
-14
lines changed

6 files changed

+166
-14
lines changed

.github/workflows/deploy.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ jobs:
5757

5858
- name: 🏗️ Build production output
5959
run: just build
60+
env:
61+
NEXT_PUBLIC_SITE_URL: https://josimar-silva.com
6062

6163
- name: 🚀 Deploy to Cloudflare Pages
6264
id: deploy

e2e-tests/share-modal.spec.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { test, expect } from "./_shared/fixtures";
2+
import { Locator, Page } from "@playwright/test";
3+
4+
async function openShareModal(page: Page): Promise<Locator> {
5+
const shareButton = page.getByRole("button", { name: "Share" });
6+
7+
await expect(shareButton).toBeVisible();
8+
await shareButton.click();
9+
10+
const modal = page.getByRole("dialog");
11+
await expect(modal).toBeVisible();
12+
13+
return modal;
14+
}
15+
16+
async function getSharePopupFor(label: string, page: Page) {
17+
const [popup] = await Promise.all([
18+
page.waitForEvent("popup"),
19+
page.getByLabel(label).click(),
20+
]);
21+
return popup;
22+
}
23+
24+
function getUrlParam(url: string, name: string) {
25+
const params = new URLSearchParams(new URL(url).search);
26+
return params.get(name);
27+
}
28+
29+
test.describe("ShareModal", () => {
30+
let blogPostUrl: string;
31+
let blogPostTitle: string;
32+
33+
test.beforeEach(async ({ page }) => {
34+
await page.goto("/");
35+
36+
const blogPosts = page.getByTestId("featured-post-link");
37+
const firstBlogPost = blogPosts.first();
38+
39+
blogPostUrl = (await firstBlogPost.getAttribute("href")) || "";
40+
41+
await page.goto(blogPostUrl);
42+
43+
blogPostTitle = await page.getByTestId("blog-post-title").textContent();
44+
});
45+
46+
test("should open share modal and display share options", async ({
47+
page,
48+
}) => {
49+
await expect(
50+
page.getByRole("heading", { name: blogPostTitle }),
51+
).toBeVisible();
52+
53+
const modal = await openShareModal(page);
54+
55+
await expect(modal.getByText("Share this post")).toBeVisible();
56+
await expect(
57+
modal.getByRole("button", { name: "Copy Link" }),
58+
).toBeVisible();
59+
await expect(modal.getByLabel("Share via Email")).toBeVisible();
60+
await expect(modal.getByLabel("Share on Twitter")).toBeVisible();
61+
await expect(modal.getByLabel("Share on Telegram")).toBeVisible();
62+
await expect(modal.getByLabel("Share on LinkedIn")).toBeVisible();
63+
await expect(modal.getByLabel("Share on WhatsApp")).toBeVisible();
64+
});
65+
66+
test("should copy the post link to clipboard", async ({ page }, testInfo) => {
67+
if (
68+
["webkit", "mobile-chrome", "mobile-safari", "microsoft-edge"].includes(
69+
testInfo.project.name,
70+
)
71+
) {
72+
test.skip();
73+
}
74+
75+
await openShareModal(page);
76+
77+
await page.getByRole("button", { name: "Copy Link" }).click();
78+
const clipboardText = await page.evaluate(() =>
79+
navigator.clipboard.readText(),
80+
);
81+
expect(clipboardText).toContain(blogPostUrl);
82+
});
83+
84+
test("should share on Email", async ({ page }) => {
85+
await openShareModal(page);
86+
87+
const emailButton = page.getByLabel("Share via Email");
88+
await expect(emailButton).toBeVisible();
89+
await emailButton.click();
90+
});
91+
92+
test("should share on Twitter", async ({ page }) => {
93+
await openShareModal(page);
94+
95+
const popup = await getSharePopupFor("Share on Twitter", page);
96+
97+
expect(popup.url()).toContain("x.com/intent/tweet?url=");
98+
expect(popup.url()).toContain(`${encodeURIComponent(blogPostTitle)}`);
99+
expect(popup.url()).toContain(`${encodeURIComponent(blogPostUrl)}`);
100+
});
101+
102+
test("should share on Telegram", async ({ page }) => {
103+
await openShareModal(page);
104+
105+
const popup = await getSharePopupFor("Share on Telegram", page);
106+
107+
expect(popup.url()).toContain("telegram.me/share/url?url=");
108+
expect(popup.url()).toContain(`${encodeURIComponent(blogPostTitle)}`);
109+
expect(popup.url()).toContain(`${encodeURIComponent(blogPostUrl)}`);
110+
});
111+
112+
test("should share on LinkedIn", async ({ page }) => {
113+
await openShareModal(page);
114+
115+
const popup = await getSharePopupFor("Share on LinkedIn", page);
116+
117+
const sessionRedirect = getUrlParam(popup.url(), "session_redirect");
118+
expect(sessionRedirect).not.toBeNull();
119+
120+
const decodedRedirectUrl = decodeURIComponent(sessionRedirect as string);
121+
122+
expect(decodedRedirectUrl).toContain("linkedin.com/shareArticle");
123+
expect(decodedRedirectUrl).toContain(blogPostTitle);
124+
expect(decodedRedirectUrl).toContain(blogPostUrl);
125+
});
126+
127+
test("should share on WhatsApp", async ({ page }) => {
128+
await openShareModal(page);
129+
130+
const popup = await getSharePopupFor("Share on WhatsApp", page);
131+
132+
expect(popup.url()).toContain("whatsapp.com/send");
133+
expect(popup.url()).toContain(`${encodeURIComponent(blogPostTitle)}`);
134+
expect(popup.url()).toContain(`${encodeURIComponent(blogPostUrl)}`);
135+
});
136+
});

playwright.config.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import type { GitHubActionOptions } from "@estruyf/github-actions-reporter";
55
* Read environment variables from file.
66
* https://github.com/motdotla/dotenv
77
*/
8-
// require('dotenv').config();
98

109
/**
1110
* See https://playwright.dev/docs/test-configuration.
@@ -60,7 +59,12 @@ export default defineConfig({
6059
projects: [
6160
{
6261
name: "chromium",
63-
use: { ...devices["Desktop Chrome"] },
62+
use: {
63+
...devices["Desktop Chrome"],
64+
contextOptions: {
65+
permissions: ["clipboard-write", "clipboard-read"],
66+
},
67+
},
6468
},
6569

6670
{
@@ -90,7 +94,13 @@ export default defineConfig({
9094
},
9195
{
9296
name: "google-chrome",
93-
use: { ...devices["Desktop Chrome"], channel: "chrome" },
97+
use: {
98+
...devices["Desktop Chrome"],
99+
channel: "chrome",
100+
contextOptions: {
101+
permissions: ["clipboard-write", "clipboard-read"],
102+
},
103+
},
94104
},
95105
],
96106

src/app/__components/blog/blog-post.test.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ jest.mock("./blog-post-content", () => ({
5757
}));
5858

5959
const mockPost = {
60+
slug: "test-blog-post-title",
6061
title: "Test Blog Post Title",
6162
content: "This is the content of the test blog post.",
6263
date: "2025-08-25T10:00:00Z",
@@ -104,10 +105,10 @@ describe("BlogPost", () => {
104105
expect(backButton).toHaveAttribute("href", "/blog");
105106
});
106107

107-
it("displays the Share button", () => {
108+
it("displays the ShareModal button", () => {
108109
render(<BlogPost post={mockPost} />);
109110

110-
const shareButton = screen.getByRole("button", { name: /Share/i });
111-
expect(shareButton).toBeInTheDocument();
111+
const shareModalButton = screen.getByRole("button", { name: /share/i });
112+
expect(shareModalButton).toBeInTheDocument();
112113
});
113114
});

src/app/__components/blog/blog-post.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@
2222
* SOFTWARE.
2323
*/
2424

25-
import { ArrowLeft, Calendar, Clock, Share2 } from "lucide-react";
25+
import { ArrowLeft, Calendar, Clock } from "lucide-react";
2626
import Image from "next/image";
2727
import Link from "next/link";
2828

2929
import BlogPostContent from "@/app/__components/blog/blog-post-content";
3030
import { BlogPostNavigation } from "@/app/__components/blog/blog-post-navigation";
31-
import {ShareButton} from "@/app/__components/blog/share-button";
31+
import ShareModal from "@/app/__components/blog/share-modal";
3232
import { Badge } from "@/app/__components/ui/badge";
3333
import { Button } from "@/app/__components/ui/button";
3434

@@ -70,7 +70,10 @@ export function BlogPost({ post }: Readonly<BlogPostProps>) {
7070

7171
{/* Header */}
7272
<header className="mb-8">
73-
<h1 className="text-4xl font-bold tracking-tight lg:text-5xl mb-4">
73+
<h1
74+
className="text-4xl font-bold tracking-tight lg:text-5xl mb-4"
75+
data-testid="blog-post-title"
76+
>
7477
{post.title}
7578
</h1>
7679

@@ -89,10 +92,7 @@ export function BlogPost({ post }: Readonly<BlogPostProps>) {
8992
</div>
9093
</div>
9194

92-
<Button variant="outline" size="sm">
93-
<Share2 className="mr-2 h-4 w-4" />
94-
Share
95-
</Button>
95+
<ShareModal title={post.title} slug={post.slug} />
9696
</div>
9797

9898
{/* Metadata */}

src/app/__components/featured-posts.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,10 @@ export async function FeaturedPosts() {
6363
className="overflow-hidden flex flex-col"
6464
data-testid="featured-post-item"
6565
>
66-
<Link href={`/blog/${post.slug}`}>
66+
<Link
67+
href={`/blog/${post.slug}`}
68+
data-testid="featured-post-link"
69+
>
6770
<Image
6871
src={post.image || "/assets/blog/posts/placeholder.svg"}
6972
alt={post.title}

0 commit comments

Comments
 (0)