diff --git a/.github/workflows/page-e2e-tests.yml b/.github/workflows/page-e2e-tests.yml new file mode 100644 index 0000000..6d8f561 --- /dev/null +++ b/.github/workflows/page-e2e-tests.yml @@ -0,0 +1,47 @@ +name: Page E2E Tests + +on: + workflow_dispatch: + +jobs: + test: + name: Run Playwright Tests + permissions: + contents: read + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Install Playwright Browsers + run: pnpm --filter @changes-page/page exec playwright install --with-deps chromium + + - name: Run Playwright tests + run: pnpm --filter @changes-page/page test + + - name: Upload screenshots + uses: actions/upload-artifact@v4 + if: always() + with: + name: screenshots + path: | + apps/page/screenshot.jpg + apps/page/latestPostResponse-screenshot.jpg + retention-days: 7 + if-no-files-found: ignore diff --git a/apps/page/.gitignore b/apps/page/.gitignore index 3978add..9f9e664 100644 --- a/apps/page/.gitignore +++ b/apps/page/.gitignore @@ -7,6 +7,9 @@ # testing /coverage +/test-results/ +/playwright-report/ +/playwright/.cache/ # next.js /.next/ diff --git a/apps/page/package.json b/apps/page/package.json index 9620617..c7d533a 100644 --- a/apps/page/package.json +++ b/apps/page/package.json @@ -6,7 +6,9 @@ "dev": "next dev --port 3001", "build": "mkdir -p public/v1 && minify widget/widget.js > public/v1/widget.js && next build", "start": "next start", - "lint": "next lint" + "lint": "next lint", + "test": "playwright test", + "test:headed": "playwright test --headed" }, "dependencies": { "@arcjet/next": "1.0.0-alpha.20", @@ -50,6 +52,7 @@ }, "devDependencies": { "@next/bundle-analyzer": "^13.1.6", + "@playwright/test": "^1.57.0", "@types/cors": "^2.8.13", "@types/node": "^17.0.41", "@types/nprogress": "^0.2.3", diff --git a/apps/page/playwright.config.js b/apps/page/playwright.config.js new file mode 100644 index 0000000..d671c72 --- /dev/null +++ b/apps/page/playwright.config.js @@ -0,0 +1,20 @@ +const { defineConfig, devices } = require("@playwright/test"); + +module.exports = defineConfig({ + testDir: "./tests", + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: "html", + use: { + trace: "on-first-retry", + screenshot: "only-on-failure", + }, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], +}); diff --git a/apps/page/tests/page.spec.js b/apps/page/tests/page.spec.js new file mode 100644 index 0000000..db90528 --- /dev/null +++ b/apps/page/tests/page.spec.js @@ -0,0 +1,49 @@ +const { expect, test, request } = require("@playwright/test"); + +test("visit page and take screenshot", async ({ page }) => { + const targetUrl = process.env.ENVIRONMENT_URL || "https://hey.changes.page"; + + const response = await page.goto(targetUrl); + + expect(response.status()).toBeLessThan(400); + + await page.screenshot({ path: "screenshot.jpg" }); + + const context = await request.newContext({ + baseURL: targetUrl, + }); + + const latest = await context.get(`/latest.json`); + expect(latest.ok()).toBeTruthy(); + + const latestPost = await latest.json(); + const latestPostResponse = await page.goto(latestPost.url); + expect(latestPostResponse.status()).toBeLessThan(400); + await page.screenshot({ path: "latestPostResponse-screenshot.jpg" }); +}); + +test("verify APIs", async () => { + const targetUrl = process.env.ENVIRONMENT_URL || "https://hey.changes.page"; + + const context = await request.newContext({ + baseURL: targetUrl, + }); + + const posts = await context.get(`/changes.json`); + expect(posts.ok()).toBeTruthy(); + + const robots = await context.get(`/robots.txt`); + expect(robots.ok()).toBeTruthy(); + + const sitemap = await context.get(`/sitemap.xml`); + expect(sitemap.ok()).toBeTruthy(); + + const markdown = await context.get(`/changes.md`); + expect(markdown.ok()).toBeTruthy(); + + const rss = await context.get(`/rss.xml`); + expect(rss.ok()).toBeTruthy(); + + const atom = await context.get(`/atom.xml`); + expect(atom.ok()).toBeTruthy(); +}); diff --git a/apps/web/components/marketing/features.tsx b/apps/web/components/marketing/features.tsx index 09a8ce9..370c338 100644 --- a/apps/web/components/marketing/features.tsx +++ b/apps/web/components/marketing/features.tsx @@ -93,15 +93,15 @@ export default function Features() { open-source {" "} - solution revolutionizing{" "} + platform for{" "} - changelog + changelogs {" "} and{" "} - roadmap - {" "} - management. + roadmaps + + , built for modern teams.

diff --git a/apps/web/components/marketing/hero.tsx b/apps/web/components/marketing/hero.tsx index 4f0c861..b08d7a4 100644 --- a/apps/web/components/marketing/hero.tsx +++ b/apps/web/components/marketing/hero.tsx @@ -112,11 +112,11 @@ export default function Hero({ stars = null }: { stars?: string | null }) {

- Our open-source platform empowers you to publish changelog pages and - interactive roadmaps. Share what you've built and what's - coming next. Notify users via email, gather feedback with voting, - track analytics, and enjoy a host of additional features. Kickstart - your page in just a few minutes! + Our open-source platform empowers you to share what you've + built with changelogs and what's coming next with roadmaps. + Notify users via email, gather feedback with voting, track + analytics, and enjoy a host of additional features. Kickstart your + page in just a few minutes!

diff --git a/apps/web/components/post/post.tsx b/apps/web/components/post/post.tsx index 1e203f0..188609e 100644 --- a/apps/web/components/post/post.tsx +++ b/apps/web/components/post/post.tsx @@ -8,6 +8,7 @@ import { PostType, } from "@changes-page/supabase/types/page"; import { PostDateTime, PostTypeBadge } from "@changes-page/ui"; +import { DateTime } from "@changes-page/utils"; import { Menu } from "@headlessui/react"; import { DotsVerticalIcon } from "@heroicons/react/solid"; import classNames from "classnames"; @@ -16,6 +17,7 @@ import ReactMarkdown from "react-markdown"; import rehypeRaw from "rehype-raw"; import rehypeSanitize from "rehype-sanitize"; import remarkGfm from "remark-gfm"; +import { createAuditLog } from "../../utils/auditLog"; import usePageUrl from "../../utils/hooks/usePageUrl"; import { httpGet } from "../../utils/http"; import { useUserData } from "../../utils/useUser"; @@ -23,7 +25,6 @@ import { notifyError } from "../core/toast.component"; import ConfirmDeleteDialog from "../dialogs/confirm-delete-dialog.component"; import PostOptions from "./post-options"; import { PostStatusIcon } from "./post-status"; -import { createAuditLog } from "../../utils/auditLog"; const ReactionsCounter = ({ aggregate }: { aggregate: IReactions }) => { return ( @@ -216,14 +217,36 @@ export function Post({ -
+
{(post?.tags ?? []).map((tag: PostType, idx) => ( -
+
))} {settings?.pinned_post_id === post.id && ( - + + )} + {post.status !== PostStatus.published && ( +
+ +
+ + {PostStatusToLabel[post.status]} + + {post.status === PostStatus.publish_later && post.publish_at && ( + + {DateTime.fromISO(post.publish_at).toNiceFormat()} + + )} +
+
)}
@@ -253,22 +276,25 @@ export function Post({