Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion e2e/constants/constants.ts
Original file line numberDiff line numberDiff line change
@@ -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";
Expand Down
92 changes: 84 additions & 8 deletions e2e/my-posts.spec.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
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";

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: 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}`));
}
Comment on lines +8 to +14

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance URL verification reliability.

The URL verification could be flaky. Consider these improvements:

  1. Add explicit wait for navigation completion
  2. Simplify the RegExp pattern
 async function openTab(page: Page, tabName: TabName) {
await page.goto("http://localhost:3000/my-posts");
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}`));+ const expectedUrl = `http://localhost:3000/my-posts?tab=${slug}`;+ await page.waitForURL(expectedUrl, { waitUntil: 'networkidle' });+ await expect(page).toHaveURL(expectedUrl);
}
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
asyncfunctionopenTab(page: Page,tabName: TabName){
awaitpage.goto("http://localhost:3000/my-posts");
awaitpage.getByRole("link",{name: tabName}).click();
constslug=tabName.toLowerCase();
awaitpage.waitForURL(`http://localhost:3000/my-posts?tab=${slug}`);
awaitexpect(page).toHaveURL(newRegExp(`\\/my-posts\\?tab=${slug}`));
}
asyncfunctionopenTab(page: Page,tabName: TabName){
awaitpage.goto("http://localhost:3000/my-posts");
awaitpage.getByRole("link",{name: tabName}).click();
constslug=tabName.toLowerCase();
constexpectedUrl=`http://localhost:3000/my-posts?tab=${slug}`;
awaitpage.waitForURL(expectedUrl,{waitUntil: 'networkidle'});
awaitexpect(page).toHaveURL(expectedUrl);
}


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", () => {
test("Unauthenticated users should be redirected to get-started page if they access my-posts directly", async ({
Expand DownExpand Up@@ -35,22 +57,76 @@ 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 openTab(page, "Published");
await openDeleteModal(page, title);

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 }) => {
const title = "Published Article";
await page.goto("http://localhost:3000/my-posts");
await openTab(page, "Published");
await openDeleteModal(page, title);

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 }) => {
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 openTab(page, "Published");
await expect(page.getByRole("link", { name: article.title })).toBeVisible();
await openDeleteModal(page, article.title);

await page.getByRole("button", { name: "Delete" }).click();
await expect(page.getByRole("link", { name: article.slug })).toHaveCount(0);
Comment on lines +115 to +130

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add test data cleanup and improve assertions.

The deletion test should:

  1. Clean up test data after the test
  2. Use more specific assertions
  3. Add appropriate timeouts
 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);
+ try {
await page.goto("http://localhost:3000/my-posts");
await openTab(page, "Published");
- await expect(page.getByRole("link", { name: article.title })).toBeVisible();+ await expect(+ page.getByRole("link", { name: article.title })+ ).toBeVisible({ timeout: 5000 });
await openDeleteModal(page, article.title);
await page.getByRole("button", { name: "Delete" }).click();
- await expect(page.getByRole("link", { name: article.slug })).toHaveCount(0);+ await expect(+ page.getByRole("link", { name: article.title })+ ).toBeHidden({ timeout: 5000 });+ } finally {+ // Clean up test data+ // TODO: Add cleanup logic here+ }
});

Would you like me to help implement the test data cleanup logic?

Committable suggestion skipped: line range outside the PR's diff.

});
});
205 changes: 150 additions & 55 deletions e2e/setup.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand All@@ -20,70 +21,164 @@ export const setup = async () => {
const db = drizzle(
postgres("postgresql://postgres:secret@127.0.0.1:5432/postgres"),
);

const addE2EArticleAndComment = async (
authorId: string,
commenterId: string,
) => {
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[] = [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check out the seed file in how we can generate data consistently. Will be a good follow up pr

{
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)
Expand DownExpand Up@@ -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();
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion e2e/constants/constants.ts
Original file line numberDiff line numberDiff line change
@@ -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";
Expand Down
92 changes: 84 additions & 8 deletions e2e/my-posts.spec.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
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";

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: 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}`));
}
Comment on lines +8 to +14

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance URL verification reliability.

The URL verification could be flaky. Consider these improvements:

  1. Add explicit wait for navigation completion
  2. Simplify the RegExp pattern
 async function openTab(page: Page, tabName: TabName) {
await page.goto("http://localhost:3000/my-posts");
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}`));+ const expectedUrl = `http://localhost:3000/my-posts?tab=${slug}`;+ await page.waitForURL(expectedUrl, { waitUntil: 'networkidle' });+ await expect(page).toHaveURL(expectedUrl);
}
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
asyncfunctionopenTab(page: Page,tabName: TabName){
awaitpage.goto("http://localhost:3000/my-posts");
awaitpage.getByRole("link",{name: tabName}).click();
constslug=tabName.toLowerCase();
awaitpage.waitForURL(`http://localhost:3000/my-posts?tab=${slug}`);
awaitexpect(page).toHaveURL(newRegExp(`\\/my-posts\\?tab=${slug}`));
}
asyncfunctionopenTab(page: Page,tabName: TabName){
awaitpage.goto("http://localhost:3000/my-posts");
awaitpage.getByRole("link",{name: tabName}).click();
constslug=tabName.toLowerCase();
constexpectedUrl=`http://localhost:3000/my-posts?tab=${slug}`;
awaitpage.waitForURL(expectedUrl,{waitUntil: 'networkidle'});
awaitexpect(page).toHaveURL(expectedUrl);
}


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", () => {
test("Unauthenticated users should be redirected to get-started page if they access my-posts directly", async ({
Expand DownExpand Up@@ -35,22 +57,76 @@ 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 openTab(page, "Published");
await openDeleteModal(page, title);

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 }) => {
const title = "Published Article";
await page.goto("http://localhost:3000/my-posts");
await openTab(page, "Published");
await openDeleteModal(page, title);

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 }) => {
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 openTab(page, "Published");
await expect(page.getByRole("link", { name: article.title })).toBeVisible();
await openDeleteModal(page, article.title);

await page.getByRole("button", { name: "Delete" }).click();
await expect(page.getByRole("link", { name: article.slug })).toHaveCount(0);
Comment on lines +115 to +130

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add test data cleanup and improve assertions.

The deletion test should:

  1. Clean up test data after the test
  2. Use more specific assertions
  3. Add appropriate timeouts
 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);
+ try {
await page.goto("http://localhost:3000/my-posts");
await openTab(page, "Published");
- await expect(page.getByRole("link", { name: article.title })).toBeVisible();+ await expect(+ page.getByRole("link", { name: article.title })+ ).toBeVisible({ timeout: 5000 });
await openDeleteModal(page, article.title);
await page.getByRole("button", { name: "Delete" }).click();
- await expect(page.getByRole("link", { name: article.slug })).toHaveCount(0);+ await expect(+ page.getByRole("link", { name: article.title })+ ).toBeHidden({ timeout: 5000 });+ } finally {+ // Clean up test data+ // TODO: Add cleanup logic here+ }
});

Would you like me to help implement the test data cleanup logic?

Committable suggestion skipped: line range outside the PR's diff.

});
});
205 changes: 150 additions & 55 deletions e2e/setup.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand All@@ -20,70 +21,164 @@ export const setup = async () => {
const db = drizzle(
postgres("postgresql://postgres:secret@127.0.0.1:5432/postgres"),
);

const addE2EArticleAndComment = async (
authorId: string,
commenterId: string,
) => {
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[] = [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check out the seed file in how we can generate data consistently. Will be a good follow up pr

{
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)
Expand DownExpand Up@@ -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();
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion e2e/constants/constants.ts
Original file line numberDiff line numberDiff line change
@@ -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";
Expand Down
92 changes: 84 additions & 8 deletions e2e/my-posts.spec.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
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";

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: 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}`));
}
Comment on lines +8 to +14

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance URL verification reliability.

The URL verification could be flaky. Consider these improvements:

  1. Add explicit wait for navigation completion
  2. Simplify the RegExp pattern
 async function openTab(page: Page, tabName: TabName) {
await page.goto("http://localhost:3000/my-posts");
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}`));+ const expectedUrl = `http://localhost:3000/my-posts?tab=${slug}`;+ await page.waitForURL(expectedUrl, { waitUntil: 'networkidle' });+ await expect(page).toHaveURL(expectedUrl);
}
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
asyncfunctionopenTab(page: Page,tabName: TabName){
awaitpage.goto("http://localhost:3000/my-posts");
awaitpage.getByRole("link",{name: tabName}).click();
constslug=tabName.toLowerCase();
awaitpage.waitForURL(`http://localhost:3000/my-posts?tab=${slug}`);
awaitexpect(page).toHaveURL(newRegExp(`\\/my-posts\\?tab=${slug}`));
}
asyncfunctionopenTab(page: Page,tabName: TabName){
awaitpage.goto("http://localhost:3000/my-posts");
awaitpage.getByRole("link",{name: tabName}).click();
constslug=tabName.toLowerCase();
constexpectedUrl=`http://localhost:3000/my-posts?tab=${slug}`;
awaitpage.waitForURL(expectedUrl,{waitUntil: 'networkidle'});
awaitexpect(page).toHaveURL(expectedUrl);
}


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", () => {
test("Unauthenticated users should be redirected to get-started page if they access my-posts directly", async ({
Expand DownExpand Up@@ -35,22 +57,76 @@ 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 openTab(page, "Published");
await openDeleteModal(page, title);

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 }) => {
const title = "Published Article";
await page.goto("http://localhost:3000/my-posts");
await openTab(page, "Published");
await openDeleteModal(page, title);

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 }) => {
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 openTab(page, "Published");
await expect(page.getByRole("link", { name: article.title })).toBeVisible();
await openDeleteModal(page, article.title);

await page.getByRole("button", { name: "Delete" }).click();
await expect(page.getByRole("link", { name: article.slug })).toHaveCount(0);
Comment on lines +115 to +130

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add test data cleanup and improve assertions.

The deletion test should:

  1. Clean up test data after the test
  2. Use more specific assertions
  3. Add appropriate timeouts
 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);
+ try {
await page.goto("http://localhost:3000/my-posts");
await openTab(page, "Published");
- await expect(page.getByRole("link", { name: article.title })).toBeVisible();+ await expect(+ page.getByRole("link", { name: article.title })+ ).toBeVisible({ timeout: 5000 });
await openDeleteModal(page, article.title);
await page.getByRole("button", { name: "Delete" }).click();
- await expect(page.getByRole("link", { name: article.slug })).toHaveCount(0);+ await expect(+ page.getByRole("link", { name: article.title })+ ).toBeHidden({ timeout: 5000 });+ } finally {+ // Clean up test data+ // TODO: Add cleanup logic here+ }
});

Would you like me to help implement the test data cleanup logic?

Committable suggestion skipped: line range outside the PR's diff.

});
});
205 changes: 150 additions & 55 deletions e2e/setup.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand All@@ -20,70 +21,164 @@ export const setup = async () => {
const db = drizzle(
postgres("postgresql://postgres:secret@127.0.0.1:5432/postgres"),
);

const addE2EArticleAndComment = async (
authorId: string,
commenterId: string,
) => {
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[] = [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check out the seed file in how we can generate data consistently. Will be a good follow up pr

{
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)
Expand DownExpand Up@@ -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();
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion e2e/constants/constants.ts
Original file line numberDiff line numberDiff line change
@@ -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";
Expand Down
92 changes: 84 additions & 8 deletions e2e/my-posts.spec.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
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";

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: 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}`));
}
Comment on lines +8 to +14

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance URL verification reliability.

The URL verification could be flaky. Consider these improvements:

  1. Add explicit wait for navigation completion
  2. Simplify the RegExp pattern
 async function openTab(page: Page, tabName: TabName) {
await page.goto("http://localhost:3000/my-posts");
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}`));+ const expectedUrl = `http://localhost:3000/my-posts?tab=${slug}`;+ await page.waitForURL(expectedUrl, { waitUntil: 'networkidle' });+ await expect(page).toHaveURL(expectedUrl);
}
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
asyncfunctionopenTab(page: Page,tabName: TabName){
awaitpage.goto("http://localhost:3000/my-posts");
awaitpage.getByRole("link",{name: tabName}).click();
constslug=tabName.toLowerCase();
awaitpage.waitForURL(`http://localhost:3000/my-posts?tab=${slug}`);
awaitexpect(page).toHaveURL(newRegExp(`\\/my-posts\\?tab=${slug}`));
}
asyncfunctionopenTab(page: Page,tabName: TabName){
awaitpage.goto("http://localhost:3000/my-posts");
awaitpage.getByRole("link",{name: tabName}).click();
constslug=tabName.toLowerCase();
constexpectedUrl=`http://localhost:3000/my-posts?tab=${slug}`;
awaitpage.waitForURL(expectedUrl,{waitUntil: 'networkidle'});
awaitexpect(page).toHaveURL(expectedUrl);
}


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", () => {
test("Unauthenticated users should be redirected to get-started page if they access my-posts directly", async ({
Expand DownExpand Up@@ -35,22 +57,76 @@ 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 openTab(page, "Published");
await openDeleteModal(page, title);

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 }) => {
const title = "Published Article";
await page.goto("http://localhost:3000/my-posts");
await openTab(page, "Published");
await openDeleteModal(page, title);

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 }) => {
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 openTab(page, "Published");
await expect(page.getByRole("link", { name: article.title })).toBeVisible();
await openDeleteModal(page, article.title);

await page.getByRole("button", { name: "Delete" }).click();
await expect(page.getByRole("link", { name: article.slug })).toHaveCount(0);
Comment on lines +115 to +130

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add test data cleanup and improve assertions.

The deletion test should:

  1. Clean up test data after the test
  2. Use more specific assertions
  3. Add appropriate timeouts
 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);
+ try {
await page.goto("http://localhost:3000/my-posts");
await openTab(page, "Published");
- await expect(page.getByRole("link", { name: article.title })).toBeVisible();+ await expect(+ page.getByRole("link", { name: article.title })+ ).toBeVisible({ timeout: 5000 });
await openDeleteModal(page, article.title);
await page.getByRole("button", { name: "Delete" }).click();
- await expect(page.getByRole("link", { name: article.slug })).toHaveCount(0);+ await expect(+ page.getByRole("link", { name: article.title })+ ).toBeHidden({ timeout: 5000 });+ } finally {+ // Clean up test data+ // TODO: Add cleanup logic here+ }
});

Would you like me to help implement the test data cleanup logic?

Committable suggestion skipped: line range outside the PR's diff.

});
});
205 changes: 150 additions & 55 deletions e2e/setup.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand All@@ -20,70 +21,164 @@ export const setup = async () => {
const db = drizzle(
postgres("postgresql://postgres:secret@127.0.0.1:5432/postgres"),
);

const addE2EArticleAndComment = async (
authorId: string,
commenterId: string,
) => {
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[] = [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check out the seed file in how we can generate data consistently. Will be a good follow up pr

{
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)
Expand DownExpand Up@@ -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();
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion e2e/constants/constants.ts
Original file line numberDiff line numberDiff line change
@@ -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";
Expand Down
92 changes: 84 additions & 8 deletions e2e/my-posts.spec.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
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";

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: 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}`));
}
Comment on lines +8 to +14

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance URL verification reliability.

The URL verification could be flaky. Consider these improvements:

  1. Add explicit wait for navigation completion
  2. Simplify the RegExp pattern
 async function openTab(page: Page, tabName: TabName) {
await page.goto("http://localhost:3000/my-posts");
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}`));+ const expectedUrl = `http://localhost:3000/my-posts?tab=${slug}`;+ await page.waitForURL(expectedUrl, { waitUntil: 'networkidle' });+ await expect(page).toHaveURL(expectedUrl);
}
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
asyncfunctionopenTab(page: Page,tabName: TabName){
awaitpage.goto("http://localhost:3000/my-posts");
awaitpage.getByRole("link",{name: tabName}).click();
constslug=tabName.toLowerCase();
awaitpage.waitForURL(`http://localhost:3000/my-posts?tab=${slug}`);
awaitexpect(page).toHaveURL(newRegExp(`\\/my-posts\\?tab=${slug}`));
}
asyncfunctionopenTab(page: Page,tabName: TabName){
awaitpage.goto("http://localhost:3000/my-posts");
awaitpage.getByRole("link",{name: tabName}).click();
constslug=tabName.toLowerCase();
constexpectedUrl=`http://localhost:3000/my-posts?tab=${slug}`;
awaitpage.waitForURL(expectedUrl,{waitUntil: 'networkidle'});
awaitexpect(page).toHaveURL(expectedUrl);
}


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", () => {
test("Unauthenticated users should be redirected to get-started page if they access my-posts directly", async ({
Expand DownExpand Up@@ -35,22 +57,76 @@ 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 openTab(page, "Published");
await openDeleteModal(page, title);

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 }) => {
const title = "Published Article";
await page.goto("http://localhost:3000/my-posts");
await openTab(page, "Published");
await openDeleteModal(page, title);

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 }) => {
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 openTab(page, "Published");
await expect(page.getByRole("link", { name: article.title })).toBeVisible();
await openDeleteModal(page, article.title);

await page.getByRole("button", { name: "Delete" }).click();
await expect(page.getByRole("link", { name: article.slug })).toHaveCount(0);
Comment on lines +115 to +130

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add test data cleanup and improve assertions.

The deletion test should:

  1. Clean up test data after the test
  2. Use more specific assertions
  3. Add appropriate timeouts
 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);
+ try {
await page.goto("http://localhost:3000/my-posts");
await openTab(page, "Published");
- await expect(page.getByRole("link", { name: article.title })).toBeVisible();+ await expect(+ page.getByRole("link", { name: article.title })+ ).toBeVisible({ timeout: 5000 });
await openDeleteModal(page, article.title);
await page.getByRole("button", { name: "Delete" }).click();
- await expect(page.getByRole("link", { name: article.slug })).toHaveCount(0);+ await expect(+ page.getByRole("link", { name: article.title })+ ).toBeHidden({ timeout: 5000 });+ } finally {+ // Clean up test data+ // TODO: Add cleanup logic here+ }
});

Would you like me to help implement the test data cleanup logic?

Committable suggestion skipped: line range outside the PR's diff.

});
});
205 changes: 150 additions & 55 deletions e2e/setup.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand All@@ -20,70 +21,164 @@ export const setup = async () => {
const db = drizzle(
postgres("postgresql://postgres:secret@127.0.0.1:5432/postgres"),
);

const addE2EArticleAndComment = async (
authorId: string,
commenterId: string,
) => {
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[] = [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check out the seed file in how we can generate data consistently. Will be a good follow up pr

{
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)
Expand DownExpand Up@@ -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();
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion e2e/constants/constants.ts
Original file line numberDiff line numberDiff line change
@@ -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";
Expand Down
92 changes: 84 additions & 8 deletions e2e/my-posts.spec.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
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";

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: 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}`));
}
Comment on lines +8 to +14

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance URL verification reliability.

The URL verification could be flaky. Consider these improvements:

  1. Add explicit wait for navigation completion
  2. Simplify the RegExp pattern
 async function openTab(page: Page, tabName: TabName) {
await page.goto("http://localhost:3000/my-posts");
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}`));+ const expectedUrl = `http://localhost:3000/my-posts?tab=${slug}`;+ await page.waitForURL(expectedUrl, { waitUntil: 'networkidle' });+ await expect(page).toHaveURL(expectedUrl);
}
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
asyncfunctionopenTab(page: Page,tabName: TabName){
awaitpage.goto("http://localhost:3000/my-posts");
awaitpage.getByRole("link",{name: tabName}).click();
constslug=tabName.toLowerCase();
awaitpage.waitForURL(`http://localhost:3000/my-posts?tab=${slug}`);
awaitexpect(page).toHaveURL(newRegExp(`\\/my-posts\\?tab=${slug}`));
}
asyncfunctionopenTab(page: Page,tabName: TabName){
awaitpage.goto("http://localhost:3000/my-posts");
awaitpage.getByRole("link",{name: tabName}).click();
constslug=tabName.toLowerCase();
constexpectedUrl=`http://localhost:3000/my-posts?tab=${slug}`;
awaitpage.waitForURL(expectedUrl,{waitUntil: 'networkidle'});
awaitexpect(page).toHaveURL(expectedUrl);
}


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", () => {
test("Unauthenticated users should be redirected to get-started page if they access my-posts directly", async ({
Expand DownExpand Up@@ -35,22 +57,76 @@ 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 openTab(page, "Published");
await openDeleteModal(page, title);

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 }) => {
const title = "Published Article";
await page.goto("http://localhost:3000/my-posts");
await openTab(page, "Published");
await openDeleteModal(page, title);

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 }) => {
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 openTab(page, "Published");
await expect(page.getByRole("link", { name: article.title })).toBeVisible();
await openDeleteModal(page, article.title);

await page.getByRole("button", { name: "Delete" }).click();
await expect(page.getByRole("link", { name: article.slug })).toHaveCount(0);
Comment on lines +115 to +130

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add test data cleanup and improve assertions.

The deletion test should:

  1. Clean up test data after the test
  2. Use more specific assertions
  3. Add appropriate timeouts
 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);
+ try {
await page.goto("http://localhost:3000/my-posts");
await openTab(page, "Published");
- await expect(page.getByRole("link", { name: article.title })).toBeVisible();+ await expect(+ page.getByRole("link", { name: article.title })+ ).toBeVisible({ timeout: 5000 });
await openDeleteModal(page, article.title);
await page.getByRole("button", { name: "Delete" }).click();
- await expect(page.getByRole("link", { name: article.slug })).toHaveCount(0);+ await expect(+ page.getByRole("link", { name: article.title })+ ).toBeHidden({ timeout: 5000 });+ } finally {+ // Clean up test data+ // TODO: Add cleanup logic here+ }
});

Would you like me to help implement the test data cleanup logic?

Committable suggestion skipped: line range outside the PR's diff.

});
});
205 changes: 150 additions & 55 deletions e2e/setup.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand All@@ -20,70 +21,164 @@ export const setup = async () => {
const db = drizzle(
postgres("postgresql://postgres:secret@127.0.0.1:5432/postgres"),
);

const addE2EArticleAndComment = async (
authorId: string,
commenterId: string,
) => {
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[] = [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check out the seed file in how we can generate data consistently. Will be a good follow up pr

{
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)
Expand DownExpand Up@@ -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();
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion e2e/constants/constants.ts
Original file line numberDiff line numberDiff line change
@@ -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";
Expand Down
92 changes: 84 additions & 8 deletions e2e/my-posts.spec.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
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";

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: 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}`));
}
Comment on lines +8 to +14

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance URL verification reliability.

The URL verification could be flaky. Consider these improvements:

  1. Add explicit wait for navigation completion
  2. Simplify the RegExp pattern
 async function openTab(page: Page, tabName: TabName) {
await page.goto("http://localhost:3000/my-posts");
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}`));+ const expectedUrl = `http://localhost:3000/my-posts?tab=${slug}`;+ await page.waitForURL(expectedUrl, { waitUntil: 'networkidle' });+ await expect(page).toHaveURL(expectedUrl);
}
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
asyncfunctionopenTab(page: Page,tabName: TabName){
awaitpage.goto("http://localhost:3000/my-posts");
awaitpage.getByRole("link",{name: tabName}).click();
constslug=tabName.toLowerCase();
awaitpage.waitForURL(`http://localhost:3000/my-posts?tab=${slug}`);
awaitexpect(page).toHaveURL(newRegExp(`\\/my-posts\\?tab=${slug}`));
}
asyncfunctionopenTab(page: Page,tabName: TabName){
awaitpage.goto("http://localhost:3000/my-posts");
awaitpage.getByRole("link",{name: tabName}).click();
constslug=tabName.toLowerCase();
constexpectedUrl=`http://localhost:3000/my-posts?tab=${slug}`;
awaitpage.waitForURL(expectedUrl,{waitUntil: 'networkidle'});
awaitexpect(page).toHaveURL(expectedUrl);
}


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", () => {
test("Unauthenticated users should be redirected to get-started page if they access my-posts directly", async ({
Expand DownExpand Up@@ -35,22 +57,76 @@ 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 openTab(page, "Published");
await openDeleteModal(page, title);

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 }) => {
const title = "Published Article";
await page.goto("http://localhost:3000/my-posts");
await openTab(page, "Published");
await openDeleteModal(page, title);

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 }) => {
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 openTab(page, "Published");
await expect(page.getByRole("link", { name: article.title })).toBeVisible();
await openDeleteModal(page, article.title);

await page.getByRole("button", { name: "Delete" }).click();
await expect(page.getByRole("link", { name: article.slug })).toHaveCount(0);
Comment on lines +115 to +130

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add test data cleanup and improve assertions.

The deletion test should:

  1. Clean up test data after the test
  2. Use more specific assertions
  3. Add appropriate timeouts
 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);
+ try {
await page.goto("http://localhost:3000/my-posts");
await openTab(page, "Published");
- await expect(page.getByRole("link", { name: article.title })).toBeVisible();+ await expect(+ page.getByRole("link", { name: article.title })+ ).toBeVisible({ timeout: 5000 });
await openDeleteModal(page, article.title);
await page.getByRole("button", { name: "Delete" }).click();
- await expect(page.getByRole("link", { name: article.slug })).toHaveCount(0);+ await expect(+ page.getByRole("link", { name: article.title })+ ).toBeHidden({ timeout: 5000 });+ } finally {+ // Clean up test data+ // TODO: Add cleanup logic here+ }
});

Would you like me to help implement the test data cleanup logic?

Committable suggestion skipped: line range outside the PR's diff.

});
});
205 changes: 150 additions & 55 deletions e2e/setup.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand All@@ -20,70 +21,164 @@ export const setup = async () => {
const db = drizzle(
postgres("postgresql://postgres:secret@127.0.0.1:5432/postgres"),
);

const addE2EArticleAndComment = async (
authorId: string,
commenterId: string,
) => {
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[] = [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check out the seed file in how we can generate data consistently. Will be a good follow up pr

{
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)
Expand DownExpand Up@@ -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();
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion e2e/constants/constants.ts
Original file line numberDiff line numberDiff line change
@@ -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";
Expand Down
92 changes: 84 additions & 8 deletions e2e/my-posts.spec.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
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";

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: 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}`));
}
Comment on lines +8 to +14

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance URL verification reliability.

The URL verification could be flaky. Consider these improvements:

  1. Add explicit wait for navigation completion
  2. Simplify the RegExp pattern
 async function openTab(page: Page, tabName: TabName) {
await page.goto("http://localhost:3000/my-posts");
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}`));+ const expectedUrl = `http://localhost:3000/my-posts?tab=${slug}`;+ await page.waitForURL(expectedUrl, { waitUntil: 'networkidle' });+ await expect(page).toHaveURL(expectedUrl);
}
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
asyncfunctionopenTab(page: Page,tabName: TabName){
awaitpage.goto("http://localhost:3000/my-posts");
awaitpage.getByRole("link",{name: tabName}).click();
constslug=tabName.toLowerCase();
awaitpage.waitForURL(`http://localhost:3000/my-posts?tab=${slug}`);
awaitexpect(page).toHaveURL(newRegExp(`\\/my-posts\\?tab=${slug}`));
}
asyncfunctionopenTab(page: Page,tabName: TabName){
awaitpage.goto("http://localhost:3000/my-posts");
awaitpage.getByRole("link",{name: tabName}).click();
constslug=tabName.toLowerCase();
constexpectedUrl=`http://localhost:3000/my-posts?tab=${slug}`;
awaitpage.waitForURL(expectedUrl,{waitUntil: 'networkidle'});
awaitexpect(page).toHaveURL(expectedUrl);
}


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", () => {
test("Unauthenticated users should be redirected to get-started page if they access my-posts directly", async ({
Expand DownExpand Up@@ -35,22 +57,76 @@ 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 openTab(page, "Published");
await openDeleteModal(page, title);

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 }) => {
const title = "Published Article";
await page.goto("http://localhost:3000/my-posts");
await openTab(page, "Published");
await openDeleteModal(page, title);

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 }) => {
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 openTab(page, "Published");
await expect(page.getByRole("link", { name: article.title })).toBeVisible();
await openDeleteModal(page, article.title);

await page.getByRole("button", { name: "Delete" }).click();
await expect(page.getByRole("link", { name: article.slug })).toHaveCount(0);
Comment on lines +115 to +130

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add test data cleanup and improve assertions.

The deletion test should:

  1. Clean up test data after the test
  2. Use more specific assertions
  3. Add appropriate timeouts
 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);
+ try {
await page.goto("http://localhost:3000/my-posts");
await openTab(page, "Published");
- await expect(page.getByRole("link", { name: article.title })).toBeVisible();+ await expect(+ page.getByRole("link", { name: article.title })+ ).toBeVisible({ timeout: 5000 });
await openDeleteModal(page, article.title);
await page.getByRole("button", { name: "Delete" }).click();
- await expect(page.getByRole("link", { name: article.slug })).toHaveCount(0);+ await expect(+ page.getByRole("link", { name: article.title })+ ).toBeHidden({ timeout: 5000 });+ } finally {+ // Clean up test data+ // TODO: Add cleanup logic here+ }
});

Would you like me to help implement the test data cleanup logic?

Committable suggestion skipped: line range outside the PR's diff.

});
});
205 changes: 150 additions & 55 deletions e2e/setup.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand All@@ -20,70 +21,164 @@ export const setup = async () => {
const db = drizzle(
postgres("postgresql://postgres:secret@127.0.0.1:5432/postgres"),
);

const addE2EArticleAndComment = async (
authorId: string,
commenterId: string,
) => {
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[] = [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check out the seed file in how we can generate data consistently. Will be a good follow up pr

{
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)
Expand DownExpand Up@@ -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();
Expand Down
Loading