From 6cd0e8076f2eed11c5bebce4a698642a9103f08a Mon Sep 17 00:00:00 2001 From: Patrick Hladun Date: Sat, 2 Nov 2024 14:10:31 +0000 Subject: [PATCH 1/5] test: add e2e tests for managing delete modal interactions (#1169) - Implement tests to ensure the delete modal can be closed with both 'Cancel' and 'Close' buttons. - Add test to verify deletion of a published article through the delete modal. - Introduce utility functions `openPublishedTab` and `openDeleteModal` to streamline modal interactions in tests. --- e2e/my-posts.spec.ts | 62 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/e2e/my-posts.spec.ts b/e2e/my-posts.spec.ts index 38cec7ea..1ef2182a 100644 --- a/e2e/my-posts.spec.ts +++ b/e2e/my-posts.spec.ts @@ -1,6 +1,27 @@ +import type { Page } from "@playwright/test"; import test, { expect } from "@playwright/test"; import { articleExcerpt, loggedInAsUserOne } from "./utils"; +async function openPublishedTab(page: Page) { + await page.goto("http://localhost:3000/my-posts"); + await page.getByRole("link", { name: "Published" }).click(); + await expect(page).toHaveURL(/\/my-posts\?tab=published$/); +} + +async function openDeleteModal(page: Page) { + await page.getByRole("button", { name: "Options" }).click(); + const optionsDiv = page.locator( + "div[aria-labelledby='headlessui-menu-button-:r5:']", + ); + await expect(optionsDiv).toBeVisible(); + const deleteButton = optionsDiv.locator("text=Delete"); + await deleteButton.click(); + const confirmationDiv = page.getByText( + "Are you sure you want to delete this article?", + ); + await expect(confirmationDiv).toBeVisible(); +} + test.describe("Unauthenticated my-posts Page", () => { test("Unauthenticated users should be redirected to get-started page if they access my-posts directly", async ({ page, @@ -53,4 +74,45 @@ test.describe("Authenticated my-posts Page", () => { ).toBeVisible(); await expect(page.getByText(articleExcerpt, { exact: true })).toBeVisible(); }); + + test("User should close delete modal with Cancel button", async ({ + page, + }) => { + await page.goto("http://localhost:3000/my-posts"); + await openPublishedTab(page); + await openDeleteModal(page); + + const closeButton = page.getByRole("button", { name: "Cancel" }); + await closeButton.click(); + + await expect( + page.locator("text=Are you sure you want to delete this article?"), + ).toBeHidden(); + }); + + test("User should close delete modal with Close button", async ({ page }) => { + await page.goto("http://localhost:3000/my-posts"); + await openPublishedTab(page); + await openDeleteModal(page); + + const closeButton = page.getByRole("button", { name: "Close" }); + await closeButton.click(); + + await expect( + page.locator("text=Are you sure you want to delete this article?"), + ).toBeHidden(); + }); + + test("User should delete published article", async ({ page }) => { + await page.goto("http://localhost:3000/my-posts"); + await openPublishedTab(page); + await openDeleteModal(page); + + const closeButton = page.getByRole("button", { name: "Delete" }); + await closeButton.click(); + + await expect( + page.getByRole("link", { name: "/articles/e2e-test-slug-published" }), + ).toHaveCount(0); + }); }); From f85a64d11b648100075e947e24df165ab6667c3d Mon Sep 17 00:00:00 2001 From: Patrick Hladun Date: Mon, 4 Nov 2024 10:31:22 +0000 Subject: [PATCH 2/5] feat(utils): add createArticle function for setting up test articles --- e2e/utils/utils.ts | 51 ++++++++++++++++++++++++++++++++++++++++++++-- types/types.ts | 13 ++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/e2e/utils/utils.ts b/e2e/utils/utils.ts index 408f16e6..01c7b087 100644 --- a/e2e/utils/utils.ts +++ b/e2e/utils/utils.ts @@ -1,5 +1,14 @@ -import { expect, Page } from "@playwright/test"; -import { E2E_USER_ONE_SESSION_ID, E2E_USER_TWO_SESSION_ID } from "../constants"; +import { post } from "@/server/db/schema"; +import { expect } from "@playwright/test"; +import type { Page } from "@playwright/test"; +import { drizzle } from "drizzle-orm/postgres-js"; +import postgres from "postgres"; +import { + E2E_USER_ONE_SESSION_ID, + E2E_USER_TWO_SESSION_ID, + E2E_USER_ONE_ID, +} from "../constants"; +import type { Article } from "@/types/types"; export const loggedInAsUserOne = async (page: Page) => { try { @@ -46,3 +55,41 @@ export const loggedInAsUserTwo = async (page: Page) => { throw Error("Error while authenticating E2E test user two"); } }; + +export async function createArticle({ + id, + title, + slug, + excerpt, + body, + likes = 10, + readTimeMins = 3, + published = new Date().toISOString(), + updatedAt = new Date().toISOString(), + userId = E2E_USER_ONE_ID, +}: Partial
) { + const db = drizzle( + postgres("postgresql://postgres:secret@127.0.0.1:5432/postgres"), + ); + + try { + await db + .insert(post) + .values({ + id, + title, + slug, + excerpt, + body, + likes, + readTimeMins, + published, + updatedAt, + userId, + } as Article) + .onConflictDoNothing() + .returning(); + } catch (err) { + throw Error("Error while creating E2E test article"); + } +} diff --git a/types/types.ts b/types/types.ts index dc4973fe..bedf0185 100644 --- a/types/types.ts +++ b/types/types.ts @@ -11,3 +11,16 @@ export type UserNavigationItem = href?: undefined; fancy?: boolean; }; + +export interface Article { + id: string; + title: string; + slug: string; + excerpt: string; + body: string; + likes: number; + readTimeMins: number; + published: string | null; + updatedAt: string; + userId: string; +} \ No newline at end of file From 66e7450a80d5c1e9c0075879fe078b2b70815c21 Mon Sep 17 00:00:00 2001 From: Patrick Hladun Date: Mon, 4 Nov 2024 10:37:21 +0000 Subject: [PATCH 3/5] feat: enhance test setup with additional articles in setup.ts --- e2e/constants/constants.ts | 2 +- e2e/setup.ts | 205 +++++++++++++++++++++++++++---------- 2 files changed, 151 insertions(+), 56 deletions(-) diff --git a/e2e/constants/constants.ts b/e2e/constants/constants.ts index c5d853ac..2f906d90 100644 --- a/e2e/constants/constants.ts +++ b/e2e/constants/constants.ts @@ -1,7 +1,7 @@ export const articleContent = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas vitae ipsum id metus vestibulum rutrum eget a diam. Integer eget vulputate risus, ac convallis nulla. Mauris sed augue nunc. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nam congue posuere tempor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Ut ac augue non libero ullamcorper ornare. Ut commodo ligula vitae malesuada maximus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Etiam sagittis justo non justo placerat, a dapibus sapien volutpat. Nullam ullamcorper sodales justo sed."; -export const articleExcerpt = "Lorem ipsum dolor sit amet"; +export const articleExcerpt = "This is an excerpt for a published article."; export const E2E_USER_ONE_EMAIL = "e2e@codu.co"; export const E2E_USER_ONE_ID = "8e3179ce-f32b-4d0a-ba3b-234d66b836ad"; diff --git a/e2e/setup.ts b/e2e/setup.ts index 36382a62..9a456524 100644 --- a/e2e/setup.ts +++ b/e2e/setup.ts @@ -12,6 +12,7 @@ import { E2E_USER_TWO_SESSION_ID, } from "./constants"; import { eq } from "drizzle-orm"; +import type { Article } from "@/types/types"; export const setup = async () => { // Dynamically import nanoid @@ -20,6 +21,7 @@ export const setup = async () => { const db = drizzle( postgres("postgresql://postgres:secret@127.0.0.1:5432/postgres"), ); + const addE2EArticleAndComment = async ( authorId: string, commenterId: string, @@ -27,63 +29,156 @@ export const setup = async () => { const publishedPostId = nanoid(8); const scheduledPostId = nanoid(8); const draftPostId = nanoid(8); - const now = new Date().toISOString(); - const oneYearFromToday = new Date(now); - oneYearFromToday.setFullYear(oneYearFromToday.getFullYear() + 1); - - await Promise.all([ - db - .insert(post) - .values({ - id: publishedPostId, - published: now, - excerpt: articleExcerpt, - updatedAt: now, - slug: "e2e-test-slug-published", - likes: 10, - readTimeMins: 3, - title: "Published Article", - body: articleContent, - userId: authorId, - }) - .onConflictDoNothing() - .returning(), + const now = new Date().toISOString(); + const scheduled = new Date( + new Date().setFullYear(new Date().getFullYear() + 1), + ).toISOString(); - db - .insert(post) - .values({ - id: draftPostId, - published: null, - excerpt: articleExcerpt, - updatedAt: now, - slug: "e2e-test-slug-draft", - likes: 10, - readTimeMins: 3, - title: "Draft Article", - body: articleContent, - userId: authorId, - }) - .onConflictDoNothing() - .returning(), + const articles: Article[] = [ + { + id: publishedPostId, + title: "Published Article", + slug: "e2e-test-slug-published", + excerpt: articleExcerpt, + body: articleContent, + likes: 0, + readTimeMins: 2, + published: now, + updatedAt: now, + userId: authorId, + }, + { + id: scheduledPostId, + title: "Scheduled Article", + slug: "e2e-test-slug-scheduled", + excerpt: "This is an excerpt for a scheduled article.", + body: "This is the body for a scheduled article.", + likes: 0, + readTimeMins: 2, + published: scheduled, + updatedAt: now, + userId: authorId, + }, + { + id: draftPostId, + title: "Draft Article", + slug: "e2e-test-slug-draft", + excerpt: "This is an excerpt for a draft article.", + body: "This is the body for a draft article.", + likes: 0, + readTimeMins: 2, + published: null, + updatedAt: now, + userId: authorId, + }, + { + id: nanoid(8), + title: "Next.js Best Practices", + slug: "e2e-nextjs-best-practices", + excerpt: + "Optimize your Next.js applications with these best practices.", + body: "This guide explores how to structure your Next.js projects effectively, utilize Server-Side Rendering (SSR) and Static Site Generation (SSG) to enhance performance, and make the most of API routes to handle server-side logic.", + likes: 20, + readTimeMins: 4, + published: now, + updatedAt: now, + userId: authorId, + }, + { + id: nanoid(8), + title: "Understanding HTML5 Semantics", + slug: "e2e-understanding-html5-semantics", + excerpt: "Master the use of semantic tags in HTML5.", + body: "Semantic HTML5 elements are foundational to web accessibility and search engine optimization.", + likes: 15, + readTimeMins: 3, + published: now, + updatedAt: now, + userId: authorId, + }, + { + id: nanoid(8), + title: "JavaScript ES6 Features", + slug: "e2e-javascript-es6-features", + excerpt: "Discover the powerful features of ES6.", + body: "ECMAScript 6 introduces a wealth of new features to JavaScript, revolutionizing how developers write JS.", + likes: 25, + readTimeMins: 5, + published: null, + updatedAt: now, + userId: authorId, + }, + { + id: nanoid(8), + title: "CSS Grid vs. Flexbox", + slug: "e2e-css-grid-vs-flexbox", + excerpt: "Choosing between CSS Grid and Flexbox.", + body: "CSS Grid and Flexbox are powerful tools for creating responsive layouts.", + likes: 18, + readTimeMins: 4, + published: null, + updatedAt: now, + userId: authorId, + }, + { + id: nanoid(8), + title: "React Hooks Explained", + slug: "e2e-react-hooks-explained", + excerpt: "Simplify your React code with Hooks.", + body: "React Hooks provide a robust solution to use state and other React features without writing a class.", + likes: 22, + readTimeMins: 5, + published: scheduled, + updatedAt: now, + userId: authorId, + }, + { + id: nanoid(8), + title: "Web Accessibility Fundamentals", + slug: "e2e-web-accessibility-fundamentals", + excerpt: "Essential guidelines for web accessibility.", + body: "Creating accessible websites is a critical aspect of modern web development.", + likes: 20, + readTimeMins: 3, + published: scheduled, + updatedAt: now, + userId: authorId, + }, + ]; - db - .insert(post) - .values({ - id: scheduledPostId, - published: oneYearFromToday.toISOString(), - excerpt: articleExcerpt, - updatedAt: now, - slug: "e2e-test-slug-scheduled", - likes: 10, - readTimeMins: 3, - title: "Scheduled Article", - body: articleContent, - userId: authorId, - }) - .onConflictDoNothing() - .returning(), - ]); + await Promise.all( + articles.map( + ({ + id, + title, + slug, + excerpt, + body, + likes, + readTimeMins, + published, + updatedAt, + userId, + }) => + db + .insert(post) + .values({ + id, + title, + slug, + excerpt, + body, + likes, + readTimeMins, + published, + updatedAt, + userId, + }) + .onConflictDoNothing() + .returning(), + ), + ); await db .insert(comment) @@ -119,7 +214,7 @@ export const setup = async () => { email, image: `https://robohash.org/${encodeURIComponent(name)}?bgset=bg1`, location: "Ireland", - bio: "Hi I am an robot", + bio: "Hi I am a robot", websiteUrl: "codu.co", }; const [createdUser] = await db.insert(user).values(userData).returning(); From b5817df75a2630c69b4b0ce05647a1064186798d Mon Sep 17 00:00:00 2001 From: Patrick Hladun Date: Mon, 4 Nov 2024 10:44:46 +0000 Subject: [PATCH 4/5] fix: improve selectors and function names, update delete test --- e2e/my-posts.spec.ts | 84 ++++++++++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 35 deletions(-) diff --git a/e2e/my-posts.spec.ts b/e2e/my-posts.spec.ts index 1ef2182a..41119f5e 100644 --- a/e2e/my-posts.spec.ts +++ b/e2e/my-posts.spec.ts @@ -1,25 +1,26 @@ import type { Page } from "@playwright/test"; import test, { expect } from "@playwright/test"; -import { articleExcerpt, loggedInAsUserOne } from "./utils"; +import { loggedInAsUserOne, createArticle } from "./utils"; +import { articleExcerpt } from "./constants"; -async function openPublishedTab(page: Page) { +type TabName = "Drafts" | "Scheduled" | "Published"; + +async function openTab(page: Page, tabName: TabName) { await page.goto("http://localhost:3000/my-posts"); - await page.getByRole("link", { name: "Published" }).click(); - await expect(page).toHaveURL(/\/my-posts\?tab=published$/); + await page.getByRole("link", { name: tabName }).click(); + const slug = tabName.toLowerCase(); + await page.waitForURL(`http://localhost:3000/my-posts?tab=${slug}`); + await expect(page).toHaveURL(new RegExp(`\\/my-posts\\?tab=${slug}`)); } -async function openDeleteModal(page: Page) { - await page.getByRole("button", { name: "Options" }).click(); - const optionsDiv = page.locator( - "div[aria-labelledby='headlessui-menu-button-:r5:']", - ); - await expect(optionsDiv).toBeVisible(); - const deleteButton = optionsDiv.locator("text=Delete"); - await deleteButton.click(); - const confirmationDiv = page.getByText( - "Are you sure you want to delete this article?", - ); - await expect(confirmationDiv).toBeVisible(); +async function openDeleteModal(page: Page, title: string) { + const article = page.locator(`article:has-text("${title}")`); + await expect(article).toBeVisible(); + await article.locator("button.dropdown-button").click(); + await article.locator('text="Delete"').click(); + await expect( + page.getByText("Are you sure you want to delete this article?"), + ).toBeVisible(); } test.describe("Unauthenticated my-posts Page", () => { @@ -56,31 +57,38 @@ test.describe("Authenticated my-posts Page", () => { await expect(page.getByRole("link", { name: "Scheduled" })).toBeVisible(); await expect(page.getByRole("link", { name: "Published" })).toBeVisible(); - await page.getByRole("link", { name: "Drafts" }).click(); + await openTab(page, "Published"); await expect( - page.getByRole("heading", { name: "Draft Article" }), + page.getByRole("heading", { name: "Published Article" }), ).toBeVisible(); await expect(page.getByText(articleExcerpt)).toBeVisible(); - await page.getByRole("link", { name: "Scheduled" }).click(); + await openTab(page, "Scheduled"); await expect( page.getByRole("heading", { name: "Scheduled Article" }), ).toBeVisible(); - await expect(page.getByText(articleExcerpt)).toBeVisible(); + await expect( + page.getByText("This is an excerpt for a scheduled article."), + ).toBeVisible(); - await page.getByRole("link", { name: "Published" }).click(); + await openTab(page, "Drafts"); await expect( - page.getByRole("heading", { name: "Published Article" }), + page.getByRole("heading", { name: "Draft Article" }), + ).toBeVisible(); + await expect( + page.getByText("This is an excerpt for a draft article.", { + exact: true, + }), ).toBeVisible(); - await expect(page.getByText(articleExcerpt, { exact: true })).toBeVisible(); }); test("User should close delete modal with Cancel button", async ({ page, }) => { + const title = "Published Article"; await page.goto("http://localhost:3000/my-posts"); - await openPublishedTab(page); - await openDeleteModal(page); + await openTab(page, "Published"); + await openDeleteModal(page, title); const closeButton = page.getByRole("button", { name: "Cancel" }); await closeButton.click(); @@ -91,9 +99,10 @@ test.describe("Authenticated my-posts Page", () => { }); test("User should close delete modal with Close button", async ({ page }) => { + const title = "Published Article"; await page.goto("http://localhost:3000/my-posts"); - await openPublishedTab(page); - await openDeleteModal(page); + await openTab(page, "Published"); + await openDeleteModal(page, title); const closeButton = page.getByRole("button", { name: "Close" }); await closeButton.click(); @@ -104,15 +113,20 @@ test.describe("Authenticated my-posts Page", () => { }); test("User should delete published article", async ({ page }) => { + const article = { + id: "test-id-for-deletion", + title: "Article to be deleted", + slug: "article-to-be-deleted", + excerpt: "This is an excerpt for the article to be deleted.", + body: "This is the body for the article to be deleted.", + }; + await createArticle(article); await page.goto("http://localhost:3000/my-posts"); - await openPublishedTab(page); - await openDeleteModal(page); + await openTab(page, "Published"); + await expect(page.getByRole("link", { name: article.title })).toBeVisible(); + await openDeleteModal(page, article.title); - const closeButton = page.getByRole("button", { name: "Delete" }); - await closeButton.click(); - - await expect( - page.getByRole("link", { name: "/articles/e2e-test-slug-published" }), - ).toHaveCount(0); + await page.getByRole("button", { name: "Delete" }).click(); + await expect(page.getByRole("link", { name: article.slug })).toHaveCount(0); }); }); From e33431bc418de030b3f7064d92eee19a3307bdb2 Mon Sep 17 00:00:00 2001 From: Patrick Hladun Date: Mon, 4 Nov 2024 11:54:42 +0000 Subject: [PATCH 5/5] fix: code style issue in types/types.ts --- types/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/types.ts b/types/types.ts index bedf0185..00fa52c1 100644 --- a/types/types.ts +++ b/types/types.ts @@ -23,4 +23,4 @@ export interface Article { published: string | null; updatedAt: string; userId: string; -} \ No newline at end of file +}