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
7 changes: 7 additions & 0 deletions examples/e2e/app-router/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,10 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts

# playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

29 changes: 29 additions & 0 deletions examples/e2e/app-router/e2e/after.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { expect, test } from "@playwright/test";

test("Next after", async ({ request }) => {
const initialSSG = await request.get("/api/after/ssg");
expect(initialSSG.status()).toEqual(200);
const initialSSGJson = await initialSSG.json();

// We then fire a post request that will revalidate the SSG page 5 seconds after, but should respond immediately
const dateNow = Date.now();
const revalidateSSG = await request.post("/api/after/revalidate");
expect(revalidateSSG.status()).toEqual(200);
const revalidateSSGJson = await revalidateSSG.json();
expect(revalidateSSGJson.success).toEqual(true);
// This request should take less than 5 seconds to respond
expect(Date.now() - dateNow).toBeLessThan(5000);

// We want to immediately check if the SSG page has been revalidated, it should not have been
const notRevalidatedSSG = await request.get("/api/after/ssg");
expect(notRevalidatedSSG.status()).toEqual(200);
const notRevalidatedSSGJson = await notRevalidatedSSG.json();
expect(notRevalidatedSSGJson.date).toEqual(initialSSGJson.date);

// We then wait for 5 seconds to ensure the SSG page has been revalidated
await new Promise((resolve) => setTimeout(resolve, 5000));
const revalidatedSSG = await request.get("/api/after/ssg");
expect(revalidatedSSG.status()).toEqual(200);
const revalidatedSSGJson = await revalidatedSSG.json();
expect(revalidatedSSGJson.date).not.toEqual(initialSSGJson.date);
});
29 changes: 29 additions & 0 deletions examples/e2e/app-router/e2e/api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { expect, test } from "@playwright/test";

test("API call from client", async ({ page }) => {
await page.goto("/");
await page.getByRole("link", { name: "/API" }).click();

await page.waitForURL("/api");

let el = page.getByText("API: N/A");
await expect(el).toBeVisible();

await page.getByRole("button", { name: "Call /api/client" }).click();
el = page.getByText('API: { "hello": "client" }');
await expect(el).toBeVisible();
});

test("API call from middleware", async ({ page }) => {
await page.goto("/");
await page.getByRole("link", { name: "/API" }).click();

await page.waitForURL("/api");

let el = page.getByText("API: N/A");
await expect(el).toBeVisible();

await page.getByRole("button", { name: "Call /api/middleware" }).click();
el = page.getByText('API: { "hello": "middleware" }');
await expect(el).toBeVisible();
});
75 changes: 75 additions & 0 deletions examples/e2e/app-router/e2e/config.redirect.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { expect, test } from "@playwright/test";
/**
* This tests that the "redirect" config in next.config.js works
*
* redirects: () => {
return [
{
source: "/next-config-redirect",
destination: "/config-redirect",
permanent: true,
missing: [{ type: "cookie", key: "missing-cookie" }],
},
];
},
*/
test.describe("Next Config Redirect", () => {
test("Missing cookies", async ({ page }) => {
await page.goto("/");
await page.goto("/next-config-redirect-missing");

await page.waitForURL("/config-redirect?missing=true");

const el = page.getByText("I was redirected from next.config.js", {
exact: true,
});
await expect(el).toBeVisible();
});
test("Not missing cookies", async ({ page }) => {
await page.goto("/");
await page.goto("/next-config-redirect-not-missing");

// the cookie was not missing, so no redirects
await page.waitForURL("/next-config-redirect-not-missing");

const el = page.getByText("This page could not be found.", {
exact: true,
});
await expect(el).toBeVisible();
});
test("Has cookies", async ({ page }) => {
await page.goto("/");
await page.goto("/next-config-redirect-has");

await page.waitForURL("/config-redirect?has=true");

const el = page.getByText("I was redirected from next.config.js", {
exact: true,
});
await expect(el).toBeVisible();
});
test("Has cookies with value", async ({ page }) => {
await page.goto("/");
await page.goto("/next-config-redirect-has-with-value");

await page.waitForURL("/config-redirect?hasWithValue=true");

const el = page.getByText("I was redirected from next.config.js", {
exact: true,
});
await expect(el).toBeVisible();
});
test("Has cookies with bad value", async ({ page }) => {
await page.goto("/");
await page.goto("/next-config-redirect-has-with-bad-value");

// did not redirect
await page.waitForURL("/next-config-redirect-has-with-bad-value");

// 404 not found
const el = page.getByText("This page could not be found.", {
exact: true,
});
await expect(el).toBeVisible();
});
});
27 changes: 27 additions & 0 deletions examples/e2e/app-router/e2e/headers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { expect, test } from "@playwright/test";

/**
* Tests that the headers are available in RSC and response headers
*/
test("Headers", async ({ page }) => {
const responsePromise = page.waitForResponse((response) => {
return response.status() === 200;
});
await page.goto("/headers");

const response = await responsePromise;
// Response header should be set
const headers = response.headers();
expect(headers["response-header"]).toEqual("response-header");

// The next.config.js headers should be also set in response
expect(headers["e2e-headers"]).toEqual("next.config.js");

// Request header should be available in RSC
const el = page.getByText("request-header");
await expect(el).toBeVisible();

// Both these headers should not be present cause poweredByHeader is false in appRouter
expect(headers["x-powered-by"]).toBeFalsy();
expect(headers["x-opennext"]).toBeFalsy();
});
11 changes: 11 additions & 0 deletions examples/e2e/app-router/e2e/host.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { expect, test } from "@playwright/test";

/**
* Tests that the request.url is the deployed host and not localhost
*/
test("Request.url is host", async ({ baseURL, page }) => {
await page.goto("/api/host");

const el = page.getByText(`{"url":"${baseURL}/api/host"}`);
await expect(el).toBeVisible();
});
18 changes: 18 additions & 0 deletions examples/e2e/app-router/e2e/image-optimization.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { expect, test } from "@playwright/test";

test("Image Optimization", async ({ page }) => {
await page.goto("/");

const imageResponsePromise = page.waitForResponse(/https%3A%2F%2Fopennext.js.org%2Farchitecture.png/);
await page.locator('[href="/image-optimization"]').click();
const imageResponse = await imageResponsePromise;

await page.waitForURL("/image-optimization");

const imageContentType = imageResponse.headers()["content-type"];
expect(imageContentType).toBe("image/webp");

const el = page.locator("img");
await expect(el).toHaveJSProperty("complete", true);
await expect(el).not.toHaveJSProperty("naturalWidth", 0);
});
13 changes: 13 additions & 0 deletions examples/e2e/app-router/e2e/isr.revalidate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { expect, test } from "@playwright/test";

test("Test revalidate", async ({ request }) => {
const result = await request.get("/api/isr");

expect(result.status()).toEqual(200);
const json = await result.json();
const body = json.body;

expect(json.status).toEqual(200);
expect(body.result).toEqual(true);
expect(body.cacheControl).toEqual("private, no-cache, no-store, max-age=0, must-revalidate");
});
63 changes: 63 additions & 0 deletions examples/e2e/app-router/e2e/isr.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { expect, test } from "@playwright/test";

test("Incremental Static Regeneration", async ({ page }) => {
test.setTimeout(45000);
await page.goto("/");
await page.locator("[href='/isr']").click();
// Load the page a couple times to regenerate ISR

let el = page.getByText("Time:");
// Track the static time
let time = await el.textContent();
let newTime: typeof time;
let tempTime = time;
do {
await page.waitForTimeout(1000);
await page.reload();
time = tempTime;
el = page.getByText("Time:");
newTime = await el.textContent();
tempTime = newTime;
} while (time !== newTime);
await page.reload();

await page.waitForTimeout(1000);
el = page.getByText("Time:");
const midTime = await el.textContent();
// Expect that the time is still stale
expect(midTime).toEqual(newTime);

// Wait 10 + 1 seconds for ISR to regenerate time
await page.waitForTimeout(11000);
let finalTime = newTime;
do {
await page.waitForTimeout(2000);
el = page.getByText("Time:");
finalTime = await el.textContent();
await page.reload();
} while (newTime === finalTime);

expect(newTime).not.toEqual(finalTime);
});

test("headers", async ({ page }) => {
let responsePromise = page.waitForResponse((response) => {
return response.status() === 200;
});
await page.goto("/isr");

while (true) {
const response = await responsePromise;
const headers = response.headers();

// this was set in middleware
if (headers["cache-control"] === "max-age=10, stale-while-revalidate=999") {
break;
}
await page.waitForTimeout(1000);
responsePromise = page.waitForResponse((response) => {
return response.status() === 200;
});
await page.reload();
}
});
12 changes: 12 additions & 0 deletions examples/e2e/app-router/e2e/middleware.cookies.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { expect, test } from "@playwright/test";

test("Cookies", async ({ page, context }) => {
await page.goto("/");

const cookies = await context.cookies();
const from = cookies.find(({ name }) => name === "from");
expect(from?.value).toEqual("middleware");

const love = cookies.find(({ name }) => name === "with");
expect(love?.value).toEqual("love");
});
20 changes: 20 additions & 0 deletions examples/e2e/app-router/e2e/middleware.redirect.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { expect, test } from "@playwright/test";

test("Middleware Redirect", async ({ page, context }) => {
await page.goto("/");
await page.getByRole("link", { name: "/Redirect" }).click();

// URL is immediately redirected
await page.waitForURL("/redirect-destination");
let el = page.getByText("Redirect Destination", { exact: true });
await expect(el).toBeVisible();

// Loading page should also redirect
await page.goto("/redirect");
await page.waitForURL("/redirect-destination");
expect(await context.cookies().then((res) => res.find((cookie) => cookie.name === "test")?.value)).toBe(
"success"
);
el = page.getByText("Redirect Destination", { exact: true });
await expect(el).toBeVisible();
});
16 changes: 16 additions & 0 deletions examples/e2e/app-router/e2e/middleware.rewrite.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { expect, test } from "@playwright/test";

test("Middleware Rewrite", async ({ page }) => {
await page.goto("/");
await page.getByRole("link", { name: "/Rewrite" }).click();

await page.waitForURL("/rewrite");
let el = page.getByText("Rewritten Destination", { exact: true });
await expect(el).toBeVisible();

// Loading page should also rewrite
await page.goto("/rewrite");
await page.waitForURL("/rewrite");
el = page.getByText("Rewritten Destination", { exact: true });
await expect(el).toBeVisible();
});
18 changes: 18 additions & 0 deletions examples/e2e/app-router/e2e/modals.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { expect, test } from "@playwright/test";

test("Route modal and interception", async ({ page }) => {
await page.goto("/");
await page.getByRole("link", { name: "Albums" }).click();
await page.getByRole("link", { name: "Song: I'm never gonna give you up Year: 1965" }).click();

await page.waitForURL(`/albums/Hold%20Me%20In%20Your%20Arms/I'm%20never%20gonna%20give%20you%20up`);

const modal = page.getByText("Modal", { exact: true });
await expect(modal).toBeVisible();

// Reload the page to load non intercepted modal
await page.reload();
await page.waitForURL(`/albums/Hold%20Me%20In%20Your%20Arms/I'm%20never%20gonna%20give%20you%20up`);
const notModal = page.getByText("Not Modal", { exact: true });
await expect(notModal).toBeVisible();
});
39 changes: 39 additions & 0 deletions examples/e2e/app-router/e2e/og.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { expect, test } from "@playwright/test";
import { validateMd5 } from "../../utils";

// This is the md5sums of the expected PNGs generated with `md5sum <file>`
const OG_MD5 = "6e5e794ac0c27598a331690f96f05d00";
const API_OG_MD5 = "cac95fc3e2d4d52870c0536bb18ba85b";

test("Open-graph image to be in metatags and present", async ({ page, request }) => {
await page.goto("/og");

// Wait for meta tags to be present
const ogImageSrc = await page.locator('meta[property="og:image"]').getAttribute("content");
const ogImageAlt = await page.locator('meta[property="og:image:alt"]').getAttribute("content");
const ogImageType = await page.locator('meta[property="og:image:type"]').getAttribute("content");
const ogImageWidth = await page.locator('meta[property="og:image:width"]').getAttribute("content");
const ogImageHeight = await page.locator('meta[property="og:image:height"]').getAttribute("content");

// Verify meta tag exists and is the correct values
expect(ogImageSrc).not.toBe(null);
expect(ogImageAlt).toBe("OpenNext");
expect(ogImageType).toBe("image/png");
expect(ogImageWidth).toBe("1200");
expect(ogImageHeight).toBe("630");

// Check if the image source is working
const response = await request.get(`/og/${ogImageSrc?.split("/").at(-1)}`);
expect(response.status()).toBe(200);
expect(response.headers()["content-type"]).toBe("image/png");
expect(response.headers()["cache-control"]).toBe("public, immutable, no-transform, max-age=31536000");
expect(validateMd5(await response.body(), OG_MD5)).toBe(true);
});

test("next/og (vercel/og) to work in API route", async ({ request }) => {
const response = await request.get("api/og?title=opennext");
expect(response.status()).toBe(200);
expect(response.headers()["content-type"]).toBe("image/png");
expect(response.headers()["cache-control"]).toBe("public, immutable, no-transform, max-age=31536000");
expect(validateMd5(await response.body(), API_OG_MD5)).toBe(true);
});
Loading
Loading