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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ dist
.output
playwright-report/
test-results/
e2e/.auth/

.DS_Store

Expand Down
6 changes: 5 additions & 1 deletion client/features/upload-form/components/UploadView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ export function UploadView({
const selectedOverflowTheme = OVERFLOW_THEME_OPTIONS.find(
(option) => option.value === selectedTheme,
);
const requiresTurnstile =
import.meta.env.PROD &&
import.meta.env.VITE_PUBLIC_E2E !== "1" &&
!isAuthenticated;

useEffect(() => {
if (isPermanent) {
Expand Down Expand Up @@ -437,7 +441,7 @@ export function UploadView({
!selectedFile ||
isUploading ||
!!uploadError ||
(import.meta.env.PROD && !turnstileToken)
(requiresTurnstile && !turnstileToken)
}
onClick={onUpload}
>
Expand Down
11 changes: 11 additions & 0 deletions client/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export const Route = createFileRoute("/")({

function Home() {
const { t, i18n } = useTranslation();
const [isHomeReady, setIsHomeReady] = useState(false);
const [expirationDays, setExpirationDays] = useState(30);
const [selectedTheme, setSelectedTheme] = useState<ThemeId>("default");
const [turnstileToken, setTurnstileToken] = useState<string | null>(null);
Expand All @@ -110,6 +111,10 @@ function Home() {

const { data: session } = authClient.useSession();

useEffect(() => {
setIsHomeReady(true);
}, []);

useEffect(() => {
setExpirationDays((prev) => {
// Logged in: default to Permanent (-1) if user hasn't changed it yet.
Expand Down Expand Up @@ -237,6 +242,12 @@ function Home() {

return (
<>
<div
data-testid="home-ready"
data-ready={isHomeReady ? "true" : "false"}
className="hidden"
aria-hidden="true"
/>
{/* Main Container - Conditional Styles for Split View */}
<div
className={cn(
Expand Down
32 changes: 32 additions & 0 deletions e2e/auth.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { mkdir } from "node:fs/promises";
import { dirname } from "node:path";
import { expect, test as setup } from "@playwright/test";
import { AUTH_FILE } from "./auth.shared";

const E2E_AUTH_SECRET = "mdto-e2e-auth-secret";

setup("bootstrap authenticated session", async ({ page }) => {
await mkdir(dirname(AUTH_FILE), { recursive: true });

const response = await page.request.post("/api/test/login", {
headers: {
"x-e2e-auth-secret": E2E_AUTH_SECRET,
},
data: {
name: "playwright-user",
email: "playwright-user@example.com",
},
});

expect(response.ok()).toBeTruthy();
const sessionResponse = await page.request.get("/api/test/session", {
headers: {
"x-e2e-auth-secret": E2E_AUTH_SECRET,
},
});
expect(sessionResponse.ok()).toBeTruthy();
const session = await sessionResponse.json();
expect(session.user.name).toBe("playwright-user");

await page.context().storageState({ path: AUTH_FILE });
});
1 change: 1 addition & 0 deletions e2e/auth.shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const AUTH_FILE = "e2e/.auth/user.json";
49 changes: 49 additions & 0 deletions e2e/authenticated-upload.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { fileURLToPath } from "node:url";
import { expect, test } from "@playwright/test";
import { AUTH_FILE } from "./auth.shared";
import { waitForHomeReady } from "./home.shared";

const fixturePath = fileURLToPath(
new URL("./fixtures/sample.md", import.meta.url),
);

test.use({ storageState: AUTH_FILE });

test.beforeEach(async ({ page }) => {
await page.addInitScript(() => {
window.localStorage.setItem("mdto.lang", "en");
});
});

test("authenticated upload creates a managed page for the logged-in user", async ({
page,
}) => {
await page.goto("/");
await waitForHomeReady(page);

await expect(page.getByText("playwright-user")).toBeVisible();

await page.getByTestId("upload-file-input").setInputFiles(fixturePath);
await expect(page.getByText("sample.md")).toBeVisible();

await page.getByTestId("upload-create-button").click();

const successUrlInput = page.getByTestId("success-url-input");
await expect(successUrlInput).toBeVisible();

const successUrl = await successUrlInput.inputValue();
const url = new URL(successUrl);

expect(url.origin).toBe(new URL(page.url()).origin);
expect(url.pathname).toMatch(/^\/playwright-user\/[A-Za-z0-9_-]{4}$/);

await expect(page.getByTestId("success-open-link")).toHaveAttribute(
"href",
successUrl,
);

await page.goto(successUrl);
await expect(
page.getByRole("heading", { level: 1, name: "Sample Title" }),
).toBeVisible();
});
8 changes: 8 additions & 0 deletions e2e/home.shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { expect, type Page } from "@playwright/test";

export async function waitForHomeReady(page: Page) {
await expect(page.getByTestId("home-ready")).toHaveAttribute(
"data-ready",
"true",
);
}
4 changes: 4 additions & 0 deletions e2e/public-upload.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { fileURLToPath } from "node:url";
import { expect, type Page, test } from "@playwright/test";
import { waitForHomeReady } from "./home.shared";

const fixturePath = fileURLToPath(
new URL("./fixtures/sample.md", import.meta.url),
Expand Down Expand Up @@ -44,6 +45,7 @@ test("home smoke shows upload controls in the initial state", async ({
page,
}) => {
await page.goto("/");
await waitForHomeReady(page);

await expect(page.getByTestId("upload-file-input")).toBeAttached();
await expect(page.getByTestId("upload-preview-button")).toBeDisabled();
Expand All @@ -54,6 +56,7 @@ test("preview opens and closes for an uploaded markdown file", async ({
page,
}) => {
await page.goto("/");
await waitForHomeReady(page);
await attachMarkdownFile(page);

await page.getByTestId("upload-preview-button").click();
Expand All @@ -75,6 +78,7 @@ test("anonymous upload creates a public page and exposes the raw markdown", asyn
page,
}) => {
await page.goto("/");
await waitForHomeReady(page);
await attachMarkdownFile(page);
await page.getByRole("button", { name: "GitHub" }).click();

Expand Down
18 changes: 13 additions & 5 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { defineConfig } from "@playwright/test";

const baseURL = "http://127.0.0.1:4173";
const workerCommand = process.env.CI
? "pnpm exec wrangler dev --ip 127.0.0.1 --port 8787 --local --log-level error --var ENV:dev --var BETTER_AUTH_URL:http://127.0.0.1:4173 --var TURNSTILE_SECRET_KEY:dummy-turnstile-secret --var BETTER_AUTH_SECRET:dummy-better-auth-secret-32chars --var GITHUB_CLIENT_ID:dummy-github-client-id --var GITHUB_CLIENT_SECRET:dummy-github-client-secret"
: "VITE_PUBLIC_TURNSTILE_SITE_KEY='' VITE_PUBLIC_POSTHOG_HOST='' VITE_PUBLIC_POSTHOG_KEY='' pnpm build && pnpm exec wrangler dev --ip 127.0.0.1 --port 8787 --local --log-level error --var ENV:dev --var BETTER_AUTH_URL:http://127.0.0.1:4173 --var TURNSTILE_SECRET_KEY:dummy-turnstile-secret --var BETTER_AUTH_SECRET:dummy-better-auth-secret-32chars --var GITHUB_CLIENT_ID:dummy-github-client-id --var GITHUB_CLIENT_SECRET:dummy-github-client-secret";
const clientCommand =
"VITE_PUBLIC_TURNSTILE_SITE_KEY='' VITE_PUBLIC_POSTHOG_HOST='' VITE_PUBLIC_POSTHOG_KEY='' VITE_PUBLIC_E2E=1 pnpm build && pnpm exec vite preview --host 127.0.0.1 --port 4173 --strictPort";
const workerBaseCommand =
"pnpm exec wrangler d1 migrations apply mdto --local && pnpm exec wrangler dev --ip 127.0.0.1 --port 8787 --local --log-level error --var ENV:dev --var BETTER_AUTH_URL:http://127.0.0.1:4173 --var TURNSTILE_SECRET_KEY:dummy-turnstile-secret --var BETTER_AUTH_SECRET:dummy-better-auth-secret-32chars --var GITHUB_CLIENT_ID:dummy-github-client-id --var GITHUB_CLIENT_SECRET:dummy-github-client-secret --var ENABLE_E2E_AUTH:1 --var E2E_AUTH_SECRET:mdto-e2e-auth-secret";
const workerCommand = workerBaseCommand;

export default defineConfig({
testDir: "./e2e",
Expand All @@ -18,11 +20,18 @@ export default defineConfig({
screenshot: "only-on-failure",
},
projects: [
{
name: "setup",
testMatch: /auth\.setup\.ts/,
},
{
name: "chromium",
dependencies: ["setup"],
testIgnore: /auth\.setup\.ts/,
use: {
browserName: "chromium",
headless: true,
storageState: undefined,
viewport: {
width: 1280,
height: 900,
Expand All @@ -32,8 +41,7 @@ export default defineConfig({
],
webServer: [
{
command:
"pnpm generate:themes && vite --host 127.0.0.1 --port 4173 --strictPort",
command: clientCommand,
url: baseURL,
reuseExistingServer: !process.env.CI,
timeout: 120_000,
Expand Down
2 changes: 2 additions & 0 deletions server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { sendDiscordAlert } from "./lib/discord";
import { authRouter } from "./routes/auth.route";
import { dashboardRouter } from "./routes/dashboard.route";
import { pageApiRouter } from "./routes/page-api.route";
import { testAuthRouter } from "./routes/test-auth.route";
import { viewRouter } from "./routes/view.route";
import { createContext } from "./trpc/context";
import { appRouter } from "./trpc/router";
Expand All @@ -14,6 +15,7 @@ import { isDev } from "./utils/env";
const app = new Hono<{ Bindings: Env }>();

app.route("/api/auth", authRouter);
app.route("/api/test", testAuthRouter);
app.route("/api/v1/pages", pageApiRouter);
app.all("/api/trpc/*", async (c) => {
return await fetchRequestHandler({
Expand Down
1 change: 1 addition & 0 deletions server/lib/test-auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const E2E_USER_COOKIE_NAME = "mdto_e2e_user";
Loading
Loading