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
47 changes: 47 additions & 0 deletions .github/workflows/page-e2e-tests.yml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions apps/page/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

# testing
/coverage
/test-results/
/playwright-report/
/playwright/.cache/

# next.js
/.next/
Expand Down
5 changes: 4 additions & 1 deletion apps/page/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
20 changes: 20 additions & 0 deletions apps/page/playwright.config.js
Original file line number Diff line number Diff line change
@@ -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"] },
},
],
});
49 changes: 49 additions & 0 deletions apps/page/tests/page.spec.js
Original file line number Diff line number Diff line change
@@ -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();
});
10 changes: 5 additions & 5 deletions apps/web/components/marketing/features.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,15 +93,15 @@ export default function Features() {
<span className="underline decoration-yellow-500 text-white">
open-source
</span>{" "}
solution revolutionizing{" "}
platform for{" "}
<span className="underline decoration-red-500 text-white">
changelog
changelogs
</span>{" "}
and{" "}
<span className="underline decoration-blue-500 text-white">
roadmap
</span>{" "}
management.
roadmaps
</span>
, built for modern teams.
</p>
</div>
</div>
Expand Down
10 changes: 5 additions & 5 deletions apps/web/components/marketing/hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,11 @@ export default function Hero({ stars = null }: { stars?: string | null }) {
</p>

<p className="mt-6 text-lg leading-8 text-gray-300">
Our open-source platform empowers you to publish changelog pages and
interactive roadmaps. Share what you&apos;ve built and what&apos;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&apos;ve
built with changelogs and what&apos;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!
</p>

<div className="mt-10 flex items-center gap-x-6">
Expand Down
58 changes: 42 additions & 16 deletions apps/web/components/post/post.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -16,14 +17,14 @@ 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";
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 (
Expand Down Expand Up @@ -216,14 +217,36 @@ export function Post({
<span className="inline-flex flex-col md:flex-row text-sm md:space-x-2 space-y-2 md:space-y-0 whitespace-nowrap text-gray-500 dark:text-gray-400">
<PostDateTime publishedAt={publishedAt} startWithFullDate />

<div className="flex items-center -mt-0.5">
<div className="flex items-center -mt-0.5 flex-wrap gap-2">
{(post?.tags ?? []).map((tag: PostType, idx) => (
<div key={tag} className={classNames(idx ? "ml-2" : "")}>
<div key={tag}>
<PostTypeBadge type={tag} />
</div>
))}
{settings?.pinned_post_id === post.id && (
<PostTypeBadge type="pinned" className="ml-2" />
<PostTypeBadge type="pinned" />
)}
{post.status !== PostStatus.published && (
<div className={classNames(
"flex lg:hidden items-center space-x-1.5",
post.status === PostStatus.draft && "text-slate-700 dark:text-slate-500",
post.status === PostStatus.publish_later && "text-orange-700 dark:text-orange-500"
)}>
<PostStatusIcon
status={post.status}
className="w-4 h-4"
/>
<div className="flex flex-col">
<span className="text-xs font-medium">
{PostStatusToLabel[post.status]}
</span>
{post.status === PostStatus.publish_later && post.publish_at && (
<span className="text-xs">
{DateTime.fromISO(post.publish_at).toNiceFormat()}
</span>
)}
</div>
</div>
)}
</div>

Expand Down Expand Up @@ -253,22 +276,25 @@ export function Post({
<aside className="hidden lg:block absolute top-4 -right-52 w-48">
<h2 className="sr-only">Options</h2>
<div className="space-y-5">
<div className="flex items-center space-x-2">
<span
className={classNames(
"text-sm font-medium text-green-700 dark:text-green-500",
post.status === PostStatus.draft &&
"text-slate-700 dark:text-slate-500",
post.status === PostStatus.publish_later &&
"text-orange-700 dark:text-orange-500"
)}
>
<div className={classNames(
"flex flex-col space-y-1 text-green-700 dark:text-green-500",
post.status === PostStatus.draft &&
"text-slate-700 dark:text-slate-500",
post.status === PostStatus.publish_later &&
"text-orange-700 dark:text-orange-500"
)}>
<span className="text-sm font-medium">
<PostStatusIcon
status={post.status}
className="w-5 h-5 inline-block mr-1.5"
/>
{PostStatusToLabel[post.status]}
</span>
{post.status === PostStatus.publish_later && post.publish_at && (
<span className="text-xs pl-[1.625rem]">
{DateTime.fromISO(post.publish_at).toNiceFormat()}
</span>
)}
</div>

<div className="flex items-center space-x-2">
Expand Down Expand Up @@ -328,8 +354,8 @@ export function Post({
<ReactionsCounter aggregate={reactions} />
) : null}
</div>
</div>
</li>
</div >
</li >
</>
);
}
Loading