From 5bd0469b007dceb4424ef56c779cf425e6f6fde6 Mon Sep 17 00:00:00 2001 From: Andrew Jiang Date: Sat, 12 Oct 2024 19:12:39 -0400 Subject: [PATCH] fix: update playwright tests again (#1665) --- .github/actions/ete-docs-bundle/action.yml | 45 ---- .github/actions/turbo-ignore/action.yml | 7 +- .../workflows/deploy-docs-bundle-preview.yml | 22 +- .github/workflows/deploy-docs-bundle-prod.yml | 19 +- .github/workflows/playwright.yml | 53 ++--- package.json | 1 - .../ui/components/src/FernScrollArea.scss | 5 +- packages/ui/docs-bundle/src/middleware.ts | 66 +++--- .../ui/docs-bundle/src/server/pageRoutes.ts | 8 - .../forward-proxy}/fern/docs.yml | 0 .../forward-proxy}/fern/fern.config.json | 0 .../forward-proxy}/fern/page1.mdx | 0 .../forward-proxy}/fern/page2.mdx | 0 playwright/forward-proxy/index.spec.ts | 189 ++++++++++++++++ ...hould-match-snapshot-1-chromium-darwin.txt | 8 +- playwright/skew-protection/index.test.ts | 53 +++++ test/docs-e2e/nginx-proxy/index.test.ts | 209 ------------------ test/docs-e2e/skew-protection/index.test.ts | 74 ------- test/utils/deployment-url.ts | 7 - 19 files changed, 321 insertions(+), 445 deletions(-) delete mode 100644 .github/actions/ete-docs-bundle/action.yml rename {test/docs-e2e/nginx-proxy => playwright/forward-proxy}/fern/docs.yml (100%) rename {test/docs-e2e/nginx-proxy => playwright/forward-proxy}/fern/fern.config.json (100%) rename {test/docs-e2e/nginx-proxy => playwright/forward-proxy}/fern/page1.mdx (100%) rename {test/docs-e2e/nginx-proxy => playwright/forward-proxy}/fern/page2.mdx (100%) create mode 100644 playwright/forward-proxy/index.spec.ts rename test/docs-e2e/nginx-proxy/__snapshots__/index.test.ts.snap => playwright/forward-proxy/index.spec.ts-snapshots/sitemap-xml-should-match-snapshot-1-chromium-darwin.txt (59%) create mode 100644 playwright/skew-protection/index.test.ts delete mode 100644 test/docs-e2e/nginx-proxy/index.test.ts delete mode 100644 test/docs-e2e/skew-protection/index.test.ts delete mode 100644 test/utils/deployment-url.ts diff --git a/.github/actions/ete-docs-bundle/action.yml b/.github/actions/ete-docs-bundle/action.yml deleted file mode 100644 index 2ab63a5b3f..0000000000 --- a/.github/actions/ete-docs-bundle/action.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: E2E @fern-ui/docs-bundle -description: Run Playwright tests on the @fern-ui/docs-bundle package - -inputs: - deployment_url: - description: "The URL of the deployment to test" - required: true - token: - description: "The Vercel token to use for the deployment" - required: true - fern_token: - description: "The token to use for the Fern API" - required: true - -runs: - using: "composite" - steps: - - uses: ./.github/actions/install - - - shell: bash - name: Fetch domains - run: pnpm vercel-scripts domains.txt ${{ inputs.deployment_url }} --token=${{ inputs.token }} - - - name: Install Playwright Browsers - shell: bash - run: pnpm exec playwright install --with-deps - - - name: Run Playwright tests - shell: bash - env: - DEPLOYMENT_URL: ${{ inputs.deployment_url }} - PLAYWRIGHT_JSON_OUTPUT_NAME: results.json - run: pnpm exec playwright test playwright/smoke --workers 6 --reporter json - - - name: Run E2E tests - shell: bash - env: - DEPLOYMENT_URL: ${{ inputs.deployment_url }} - FERN_TOKEN: ${{ inputs.fern_token }} - run: pnpm i -g fern-api@latest; pnpm docs:e2e - - - uses: daun/playwright-report-summary@v3 - if: always() - with: - report-file: results.json diff --git a/.github/actions/turbo-ignore/action.yml b/.github/actions/turbo-ignore/action.yml index 327171d4cb..0f8af286e0 100644 --- a/.github/actions/turbo-ignore/action.yml +++ b/.github/actions/turbo-ignore/action.yml @@ -46,10 +46,11 @@ runs: fi if [ ! -f last-deploy.txt ]; then - echo "continue=1" >> $GITHUB_OUTPUT - exit 0 + FALLBACK="main" + else + FALLBACK=$(cat last-deploy.txt) fi set +e - pnpx turbo-ignore ${{ inputs.package }} --fallback=$(cat last-deploy.txt) + pnpx turbo-ignore ${{ inputs.package }} --fallback=$FALLBACK echo "continue=$?" >> $GITHUB_OUTPUT diff --git a/.github/workflows/deploy-docs-bundle-preview.yml b/.github/workflows/deploy-docs-bundle-preview.yml index 6391fb8b1e..dd907e1209 100644 --- a/.github/workflows/deploy-docs-bundle-preview.yml +++ b/.github/workflows/deploy-docs-bundle-preview.yml @@ -30,7 +30,7 @@ jobs: deploy: needs: ignore - if: needs.ignore.outputs.continue == 1 + if: needs.ignore.outputs.continue == 1 || contains(github.event.head_commit.message, 'playwright/') || contains(github.event.head_commit.message, '.github/workflows/deploy-docs-bundle-preview') || contains(github.event.head_commit.message, '.github/workflows/playwright') runs-on: ubuntu-latest environment: name: Preview - app.buildwithfern.com @@ -96,7 +96,7 @@ jobs: uses: dawidd6/action-download-artifact@v2 if: success() && github.event.number with: - workflow: Preview @fern-ui/docs-bundle + name: analyze branch: ${{ github.event.pull_request.base.ref }} path: packages/ui/docs-bundle/.next/analyze/base @@ -140,17 +140,7 @@ jobs: - ignore - deploy # only runs on fern-prod if: always() - runs-on: ubuntu-latest - permissions: write-all # required for the playwright-report-summary action - steps: - # if the job is ignored, skip the ETE test and exit 0 so that the this job can be used for merge protection - - uses: actions/checkout@v4 - if: needs.deploy.outputs.deployment_url - - - name: Run E2E tests - uses: ./.github/actions/ete-docs-bundle - if: needs.deploy.outputs.deployment_url - with: - deployment_url: ${{ needs.deploy.outputs.deployment_url }} - token: ${{ secrets.VERCEL_TOKEN }} - fern_token: ${{ secrets.FERN_TOKEN }} + uses: ./.github/workflows/playwright.yml + permissions: write-all + with: + deployment_url: ${{ needs.deploy.outputs.deployment_url }} diff --git a/.github/workflows/deploy-docs-bundle-prod.yml b/.github/workflows/deploy-docs-bundle-prod.yml index c20164d0cf..e88650ab15 100644 --- a/.github/workflows/deploy-docs-bundle-prod.yml +++ b/.github/workflows/deploy-docs-bundle-prod.yml @@ -72,20 +72,21 @@ jobs: ete: needs: - deploy_app_buildwithfern_com # only the app.buildwithfern.com deployment is an E2E candidate but ideally all deployments should be tested - - deploy_app_ferndocs_com - - deploy_app-slash_ferndocs_com - promote if: needs.deploy_app_buildwithfern_com.outputs.deployment_url + uses: ./.github/workflows/playwright.yml + permissions: write-all + with: + deployment_url: ${{ needs.deploy_app_buildwithfern_com.outputs.deployment_url }} + + rollback: + needs: ete + if: failure() runs-on: ubuntu-latest + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/ete-docs-bundle - with: - deployment_url: ${{ needs.deploy_app_buildwithfern_com.outputs.deployment_url }} - token: ${{ secrets.VERCEL_TOKEN }} - fern_token: ${{ secrets.FERN_TOKEN }} - name: Rollback on failure # remove this step once we switch back to pre-promotion testing - if: failure() run: | echo "E2E tests failed. Rolling back deployment" pnpm vercel-scripts rollback app.buildwithfern.com --token ${{ secrets.VERCEL_TOKEN }} diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 5416be60ca..53f7fb0c99 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -1,50 +1,43 @@ -name: Playwright Tests -on: pull_request +name: Playwright env: - TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} - TURBO_TEAM: "buildwithfern" + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} FERN_TOKEN: ${{ secrets.FERN_TOKEN }} - WORKOS_API_KEY: ${{ secrets.WORKOS_API_KEY }} - WORKOS_CLIENT_ID: ${{ secrets.WORKOS_CLIENT_ID }} - # HUME_API_KEY: ${{ secrets.HUME_API_KEY }} + PLAYWRIGHT_JSON_OUTPUT_NAME: results.json + +on: + workflow_call: + inputs: + deployment_url: + type: string + description: "The URL of the deployment to test" + required: true jobs: - test: - timeout-minutes: 10 + ete: runs-on: ubuntu-latest permissions: write-all # required for the playwright-report-summary action + if: inputs.deployment_url steps: - uses: actions/checkout@v4 - - name: Install - uses: ./.github/actions/install + - uses: ./.github/actions/install - - name: Compile and build - run: pnpm turbo compile codegen build - env: - FERN_TOKEN: ${{ secrets.FERN_TOKEN }} - WORKOS_API_KEY: ${{ secrets.WORKOS_API_KEY }} - WORKOS_CLIENT_ID: ${{ secrets.WORKOS_CLIENT_ID }} + - name: Install Fern CLI (used for docs e2e) + run: pnpm i -g fern-api@latest - - name: Build next bundle - run: cd packages/ui/local-preview-bundle; pnpm turbo build + - name: Fetch domains + run: pnpm vercel-scripts domains.txt ${{ inputs.deployment_url }} - - name: Install Playwright Browsers - run: pnpm exec playwright install --with-deps + - run: pnpm build - - name: Install fern-dev API - env: - NPM_TOKEN: ${{ secrets.FERN_NPM_TOKEN }} - FERN_TOKEN: ${{ secrets.FERN_ORG_TOKEN_DEV }} - run: | - npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN - npm install -g @fern-api/fern-api-dev + - name: Install Playwright Browsers + run: pnpm exec playwright install --with-deps --browser=chromium - name: Run Playwright tests - run: PLAYWRIGHT_JSON_OUTPUT_NAME=results.json pnpm exec playwright test playwright/fixtures --reporter json + run: pnpm exec playwright test playwright --workers 1 --reporter json - uses: daun/playwright-report-summary@v3 if: always() with: - report-file: results.json + report-file: $PLAYWRIGHT_JSON_OUTPUT_NAME diff --git a/package.json b/package.json index 72bdb18179..4ec8cf42d5 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,6 @@ "docs:dev": "turbo docs:dev", "docs:build": "turbo docs:build", "docs:start": "turbo docs:start", - "docs:e2e": "vitest --run test/docs-e2e/**/* --globals", "fdr:generate": "pnpm fern generate --api fdr --local && pnpm turbo --filter=@fern-api/fdr-sdk compile" }, "devDependencies": { diff --git a/packages/ui/components/src/FernScrollArea.scss b/packages/ui/components/src/FernScrollArea.scss index f873633aba..81e80880a0 100644 --- a/packages/ui/components/src/FernScrollArea.scss +++ b/packages/ui/components/src/FernScrollArea.scss @@ -15,7 +15,10 @@ > div { display: block !important; - width: fit-content; + + // TODO: this seems to be causing a bug where the scroll area always shrinks the content + // but i'm not sure why this was introduced in the first place. + // width: fit-content; flex-grow: 1; } diff --git a/packages/ui/docs-bundle/src/middleware.ts b/packages/ui/docs-bundle/src/middleware.ts index 15f24a3896..bea18d404f 100644 --- a/packages/ui/docs-bundle/src/middleware.ts +++ b/packages/ui/docs-bundle/src/middleware.ts @@ -18,76 +18,70 @@ const CHANGELOG_PATTERN = /\.(rss|atom)$/; export const middleware: NextMiddleware = async (request) => { const xFernHost = getDocsDomainEdge(request); const nextUrl = request.nextUrl.clone(); - const headers = new Headers(request.headers); + + let pathname = extractNextDataPathname(removeTrailingSlash(request.nextUrl.pathname)); /** - * Do not rewrite 404 and 500 pages + * Correctly handle 404 and 500 pages + * so that nextjs doesn't incorrectly match this request to __next_data_catchall */ - if ( - removeTrailingSlash(request.nextUrl.pathname) === "/404" || - removeTrailingSlash(request.nextUrl.pathname) === "/500" - ) { - return NextResponse.next(); + if (pathname === "/404" || pathname === "/500" || pathname === "/_error") { + const headers = new Headers(request.headers); + + if (request.headers.get("referer")?.includes("/_next/data/")) { + headers.set("x-nextjs-data", "1"); + } + + if (pathname === request.nextUrl.pathname) { + return NextResponse.next({ request: { headers } }); + } + nextUrl.pathname = pathname; + const response = NextResponse.rewrite(nextUrl, { request: { headers } }); + response.headers.set("x-matched-path", pathname); + return response; } /** * Rewrite robots.txt */ - if (nextUrl.pathname.endsWith("/robots.txt")) { + if (pathname.endsWith("/robots.txt")) { nextUrl.pathname = "/api/fern-docs/robots.txt"; - return NextResponse.rewrite(nextUrl, { request: { headers } }); + return NextResponse.rewrite(nextUrl); } /** * Rewrite sitemap.xml */ - if (nextUrl.pathname.endsWith("/sitemap.xml")) { + if (pathname.endsWith("/sitemap.xml")) { nextUrl.pathname = "/api/fern-docs/sitemap.xml"; - return NextResponse.rewrite(nextUrl, { request: { headers } }); + return NextResponse.rewrite(nextUrl); } /** * Rewrite Posthog analytics ingestion */ - if (nextUrl.pathname.includes("/api/fern-docs/analytics/posthog")) { + if (pathname.includes("/api/fern-docs/analytics/posthog")) { return rewritePosthog(request); } /** * Rewrite API routes to /api/fern-docs */ - if (nextUrl.pathname.match(API_FERN_DOCS_PATTERN)) { + if (pathname.match(API_FERN_DOCS_PATTERN)) { nextUrl.pathname = request.nextUrl.pathname.replace(API_FERN_DOCS_PATTERN, "/api/fern-docs/"); - return NextResponse.rewrite(nextUrl, { request: { headers } }); + return NextResponse.rewrite(nextUrl); } /** * Rewrite changelog rss and atom feeds */ - const changelogFormat = request.nextUrl.pathname.match(CHANGELOG_PATTERN)?.[1]; + const changelogFormat = pathname.match(CHANGELOG_PATTERN)?.[1]; if (changelogFormat != null) { - const pathname = request.nextUrl.pathname.replace(new RegExp(`.${changelogFormat}$`), ""); + pathname = pathname.replace(new RegExp(`.${changelogFormat}$`), ""); nextUrl.pathname = "/api/fern-docs/changelog"; nextUrl.searchParams.set("format", changelogFormat); nextUrl.searchParams.set("path", pathname); - return NextResponse.rewrite(nextUrl, { request: { headers } }); - } - - const pathname = extractNextDataPathname(request.nextUrl.pathname); - - /** - * attempt to rewrite /404 and /_error data routes to the correct destination, - * otherwise nextjs will match to `__next_data_catchall`. - * - * this is important for `hardNavigate404` to work, because it relies on knowing that the destination is /404.json - */ - if ((pathname === "/404" || pathname === "/_error") && request.nextUrl.pathname.includes("/_next/data/")) { - const buildId = getBuildId(request); - nextUrl.pathname = `/_next/data/${buildId}${pathname}.json`; - if (nextUrl.pathname === request.nextUrl.pathname) { - return NextResponse.next({ request: { headers } }); - } - return NextResponse.rewrite(nextUrl, { request: { headers } }); + return NextResponse.rewrite(nextUrl); } const fernToken = request.cookies.get(COOKIE_FERN_TOKEN); @@ -142,7 +136,7 @@ export const middleware: NextMiddleware = async (request) => { nextUrl.pathname = getPageRoutePath(!isDynamic, buildId, xFernHost, pathname); - const response = NextResponse.rewrite(nextUrl, { request: { headers } }); + const response = NextResponse.rewrite(nextUrl); /** * Add x-matched-path header to the response to help with debugging @@ -157,7 +151,7 @@ export const middleware: NextMiddleware = async (request) => { */ nextUrl.pathname = getPageRoute(!isDynamic, xFernHost, pathname); - return NextResponse.rewrite(nextUrl, { request: { headers } }); + return NextResponse.rewrite(nextUrl); }; export const config = { diff --git a/packages/ui/docs-bundle/src/server/pageRoutes.ts b/packages/ui/docs-bundle/src/server/pageRoutes.ts index eba840605b..8f9608e18d 100644 --- a/packages/ui/docs-bundle/src/server/pageRoutes.ts +++ b/packages/ui/docs-bundle/src/server/pageRoutes.ts @@ -13,13 +13,5 @@ export function getPageRouteMatch(ssg: boolean, buildId: string): string { export function getPageRoutePath(ssg: boolean, buildId: string, domain: string, pathname: string): string { const dataRoute = getAssetPathFromRoute(removeTrailingSlash(pathname), ".json"); - - /** - * Special case for 404 and 500 pages - */ - if (dataRoute === "/404.json" || dataRoute === "/500.json") { - return `/_next/data/${buildId}${dataRoute}`; - } - return `/_next/data/${buildId}/${ssg ? "static" : "dynamic"}/${domain}${dataRoute}`; } diff --git a/test/docs-e2e/nginx-proxy/fern/docs.yml b/playwright/forward-proxy/fern/docs.yml similarity index 100% rename from test/docs-e2e/nginx-proxy/fern/docs.yml rename to playwright/forward-proxy/fern/docs.yml diff --git a/test/docs-e2e/nginx-proxy/fern/fern.config.json b/playwright/forward-proxy/fern/fern.config.json similarity index 100% rename from test/docs-e2e/nginx-proxy/fern/fern.config.json rename to playwright/forward-proxy/fern/fern.config.json diff --git a/test/docs-e2e/nginx-proxy/fern/page1.mdx b/playwright/forward-proxy/fern/page1.mdx similarity index 100% rename from test/docs-e2e/nginx-proxy/fern/page1.mdx rename to playwright/forward-proxy/fern/page1.mdx diff --git a/test/docs-e2e/nginx-proxy/fern/page2.mdx b/playwright/forward-proxy/fern/page2.mdx similarity index 100% rename from test/docs-e2e/nginx-proxy/fern/page2.mdx rename to playwright/forward-proxy/fern/page2.mdx diff --git a/playwright/forward-proxy/index.spec.ts b/playwright/forward-proxy/index.spec.ts new file mode 100644 index 0000000000..c9c928d707 --- /dev/null +++ b/playwright/forward-proxy/index.spec.ts @@ -0,0 +1,189 @@ +import test, { expect } from "@playwright/test"; +import execa from "execa"; +import express from "express"; +import http from "http"; +import { createProxyMiddleware } from "http-proxy-middleware"; +import { getPreviewDeploymentUrl } from "../utils"; +const origin = getPreviewDeploymentUrl(); + +const target = "https://test-nginx-proxy.docs.buildwithfern.com/subpath"; + +const app = express(); +let server: http.Server; + +function getProxyUrl() { + const address = server.address(); + if (typeof address === "string") { + return `http://${address}`; + } + return `http://localhost:${address?.port}`; +} + +test.setTimeout(120_000); + +test.beforeAll(async () => { + const result = await execa("fern", ["generate", "--docs"], { cwd: __dirname }); + expect(result.stdout).toContain(`Published docs to ${target}`); + + const headers: Record = { + "x-fern-host": new URL(target).hostname, + }; + + if (process.env.APP_BUILDWITHFERN_COM_PROTECTION_BYPASS) { + headers["x-vercel-protection-bypass"] = process.env.APP_BUILDWITHFERN_COM_PROTECTION_BYPASS; + } + + const proxyMiddleware = createProxyMiddleware({ + target: origin, + headers, + changeOrigin: true, + followRedirects: true, + autoRewrite: true, + }); + + // redirect to /subpath/capture-the-flag + app.use("/subpath/test-capture-the-flag", (_req, res) => { + res.redirect(302, "/subpath/capture-the-flag"); + }); + + app.use((req, res, next) => { + if (req.url.startsWith("/subpath") || req.url.startsWith("/_next")) { + return proxyMiddleware(req, res, next); + } else { + return next(); + } + }); + + app.use((_req, res) => { + res.status(404).send("PROXY_NOT_FOUND_ERROR"); + }); + + server = app.listen(0, () => { + // eslint-disable-next-line no-console + console.log(`Proxy app listening on ${getProxyUrl()}`); + }); +}); + +test.afterAll(async () => { + server?.close(); +}); + +test("home page 404", async ({ page }) => { + const response = await page.goto(new URL("/", getProxyUrl()).toString()); + expect(response).toBeDefined(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const definedResponse = response!; + expect(definedResponse.status()).toBe(404); + expect(await definedResponse.text()).toBe("PROXY_NOT_FOUND_ERROR"); +}); + +test("subpath should 200 and capture the flag", async ({ page }) => { + const response = await page.goto(new URL("/subpath", getProxyUrl()).toString()); + expect(response).toBeDefined(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const definedResponse = response!; + expect(definedResponse.status()).toBe(200); + expect(await definedResponse.text()).toContain("Hello world!"); + + // capture the flag + const text = page.getByText("capture-the-flag"); + expect(await text.count()).toBe(1); + await text.click(); + + await page.waitForURL(/(capture-the-flag)/); + expect(await page.content()).not.toContain("Application error"); + expect(page.url()).toEqual(`${getProxyUrl()}/subpath/capture-the-flag`); + expect(await page.content()).toContain("capture_the_flag"); +}); + +test("subpath/test-capture-the-flag should redirect to subpath/capture-the-flag", async ({ page }) => { + const response = await page.goto(new URL("/subpath/test-capture-the-flag", getProxyUrl()).toString()); + expect(response).toBeDefined(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const definedResponse = response!; + expect(definedResponse.status()).toBe(200); + expect(definedResponse.url()).toContain("/subpath/capture-the-flag"); +}); + +test("subpath/test-3 should be 404", async ({ page }) => { + const response = await page.goto(new URL("/subpath/test-3", getProxyUrl()).toString()); + expect(response).toBeDefined(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const definedResponse = response!; + expect(definedResponse.status()).toBe(404); +}); + +test("sitemap.xml should match snapshot", async ({ page }) => { + const response = await page.goto(new URL("/subpath/sitemap.xml", getProxyUrl()).toString()); + expect(response).toBeDefined(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const definedResponse = response!; + expect(definedResponse.status()).toBe(200); + expect(await definedResponse.text()).toMatchSnapshot(); +}); + +test("revalidate-all/v3 all should work", async ({ page }) => { + test.setTimeout(10_000); + const response = await page.goto(new URL("/subpath/api/fern-docs/revalidate-all/v3", getProxyUrl()).toString(), { + timeout: 10_000, + }); + expect(response).toBeDefined(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const definedResponse = response!; + expect(definedResponse.status()).toBe(200); + + const results = await definedResponse.json(); + + expect(results.successfulRevalidations).toHaveLength(2); + expect(results.failedRevalidations).toHaveLength(0); +}); + +test("revalidate-all/v3 should work with trailing slash", async ({ page }) => { + test.setTimeout(10_000); + const response = await page.goto(new URL("/subpath/api/fern-docs/revalidate-all/v3/", getProxyUrl()).toString(), { + timeout: 10_000, + }); + expect(response).toBeDefined(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const definedResponse = response!; + expect(definedResponse.status()).toBe(200); + + const results = await definedResponse.json(); + + expect(results.successfulRevalidations).toHaveLength(2); + expect(results.failedRevalidations).toHaveLength(0); +}); + +test("revalidate-all/v4 should work", async ({ page }) => { + test.setTimeout(10_000); + const response = await page.goto(new URL("/subpath/api/fern-docs/revalidate-all/v4", getProxyUrl()).toString(), { + timeout: 10_000, + }); + expect(response).toBeDefined(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const definedResponse = response!; + expect(definedResponse.status()).toBe(200); + + const results = await definedResponse.json(); + + expect(results.total).toBe(2); + expect(results.results).toHaveLength(2); + expect((results.results as object[]).map((r) => "success" in r && r.success)).toEqual([true, true]); +}); + +test("revalidate-all/v4 should work with trailing slash", async ({ page }) => { + test.setTimeout(10_000); + const response = await page.goto(new URL("/subpath/api/fern-docs/revalidate-all/v4/", getProxyUrl()).toString(), { + timeout: 10_000, + }); + expect(response).toBeDefined(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const definedResponse = response!; + expect(definedResponse.status()).toBe(200); + + const results = await definedResponse.json(); + + expect(results.total).toBe(2); + expect(results.results).toHaveLength(2); + expect((results.results as object[]).map((r) => "success" in r && r.success)).toEqual([true, true]); +}); diff --git a/test/docs-e2e/nginx-proxy/__snapshots__/index.test.ts.snap b/playwright/forward-proxy/index.spec.ts-snapshots/sitemap-xml-should-match-snapshot-1-chromium-darwin.txt similarity index 59% rename from test/docs-e2e/nginx-proxy/__snapshots__/index.test.ts.snap rename to playwright/forward-proxy/index.spec.ts-snapshots/sitemap-xml-should-match-snapshot-1-chromium-darwin.txt index 333634a963..0c14eb744b 100644 --- a/test/docs-e2e/nginx-proxy/__snapshots__/index.test.ts.snap +++ b/playwright/forward-proxy/index.spec.ts-snapshots/sitemap-xml-should-match-snapshot-1-chromium-darwin.txt @@ -1,7 +1,4 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`nginx-proxy > sitemap.xml should match snapshot 1`] = ` -" + https://test-nginx-proxy.docs.buildwithfern.com/subpath/test-1 @@ -12,5 +9,4 @@ exports[`nginx-proxy > sitemap.xml should match snapshot 1`] = ` https://test-nginx-proxy.docs.buildwithfern.com/subpath/capture-the-flag - " -`; + \ No newline at end of file diff --git a/playwright/skew-protection/index.test.ts b/playwright/skew-protection/index.test.ts new file mode 100644 index 0000000000..d7fc1f427b --- /dev/null +++ b/playwright/skew-protection/index.test.ts @@ -0,0 +1,53 @@ +import test, { expect, Request } from "@playwright/test"; +import { getPreviewDeploymentUrl } from "../utils"; + +const origin = getPreviewDeploymentUrl(); + +test("should contain ?dpl= or x-deployment-id header on all scripts and prefetch requests", async ({ + page, + context, +}) => { + await context.addCookies([{ name: "_fern_docs_preview", url: origin, value: "buildwithfern.com" }]); + + await page.goto(`${origin}/learn/home`); + + const scripts = await page.locator("script").all(); + expect(scripts.length).toBeGreaterThan(0); + + let nextScripts = 0; + let dpl: string | undefined = undefined; + for (const script of scripts) { + const src = await script.getAttribute("src"); + + if (src?.includes("/_next/")) { + nextScripts++; + expect(src).toContain("dpl=dpl_"); + if (dpl === undefined) { + dpl = src.split("dpl=")[1]; + } + } + } + + expect(nextScripts).toBeGreaterThan(0); + expect(dpl).not.toBeUndefined(); + + await page.waitForSelector("a"); + + const requests: Request[] = []; + page.on("request", async (request) => { + requests.push(request); + }); + + const link = await page.$("a[href^='/learn/']"); + expect(link).not.toBeNull(); + + await link?.hover(); + + await page.waitForTimeout(10_000); + + const allPrefetchHeaders = (await Promise.all(requests.map((req) => req.allHeaders()))).filter( + (headers) => headers["x-nextjs-data"] === "1", + ); + expect(allPrefetchHeaders.length).toBeGreaterThan(0); + expect(allPrefetchHeaders.every((headers) => headers["x-deployment-id"] === dpl)).toBe(true); +}); diff --git a/test/docs-e2e/nginx-proxy/index.test.ts b/test/docs-e2e/nginx-proxy/index.test.ts deleted file mode 100644 index 995cf93e78..0000000000 --- a/test/docs-e2e/nginx-proxy/index.test.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { ExecSyncOptions, execFileSync } from "child_process"; -import express from "express"; -import { createProxyMiddleware } from "http-proxy-middleware"; -import { chromium } from "playwright"; -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -import { getDeploymentOrigin } from "../../utils/deployment-url"; - -const origin = getDeploymentOrigin(); - -const target = "https://test-nginx-proxy.docs.buildwithfern.com/subpath"; -const proxy = "http://localhost:5050/subpath"; - -describe("nginx-proxy", () => { - const server = express(); - let browser: Awaited>; - - beforeAll(async () => { - const result = exec("fern generate --docs", { cwd: __dirname }); - expect(result).toContain(`Published docs to ${target}`); - - const headers: Record = { - "x-fern-host": new URL(target).hostname, - }; - - if (process.env.APP_BUILDWITHFERN_COM_PROTECTION_BYPASS) { - headers["x-vercel-protection-bypass"] = process.env.APP_BUILDWITHFERN_COM_PROTECTION_BYPASS; - } - - const proxyMiddleware = createProxyMiddleware({ - target: origin, - headers, - changeOrigin: true, - followRedirects: true, - autoRewrite: true, - }); - - // redirect to /subpath/capture-the-flag - server.use("/subpath/test-capture-the-flag", (_req, res) => { - res.redirect(302, "/subpath/capture-the-flag"); - }); - - server.use((req, res, next) => { - if (req.url.startsWith("/subpath")) { - return proxyMiddleware(req, res, next); - } else { - return next(); - } - }); - - server.use((_req, res) => { - res.status(404).send("PROXY_NOT_FOUND_ERROR"); - }); - - server.listen(5050, () => { - // eslint-disable-next-line no-console - console.log(`Proxy server listening on ${proxy}`); - }); - - browser = await chromium.launch(); - }); - - afterAll(async () => { - await browser?.close(); - }); - - let page: Awaited>; - beforeEach(async () => { - page = await browser.newPage(); - }); - - afterEach(async () => { - await page.close(); - }); - - it("home page 404", async () => { - const response = await page.goto("http://localhost:5050"); - expect(response).toBeDefined(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const definedResponse = response!; - expect(definedResponse.status()).toBe(404); - expect(await definedResponse.text()).toBe("PROXY_NOT_FOUND_ERROR"); - }); - - it("subpath should 200 and capture the flag", async () => { - const response = await page.goto("http://localhost:5050/subpath"); - expect(response).toBeDefined(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const definedResponse = response!; - expect(definedResponse.status()).toBe(200); - expect(await definedResponse.text()).toContain("Hello world!"); - - // capture the flag - const text = page.getByText("capture-the-flag"); - expect(await text.count()).toBe(1); - await text.click({ force: true }); - await page.waitForURL(/(capture-the-flag)/); - expect(await page.content()).not.includes("Application error"); - expect(page.url()).equals(`${proxy}/capture-the-flag`); - expect(await page.content()).toContain("capture_the_flag"); - }, 20_000); - - it("subpath/test-capture-the-flag should redirect to subpath/capture-the-flag", async () => { - const response = await page.goto("http://localhost:5050/subpath/test-capture-the-flag"); - expect(response).toBeDefined(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const definedResponse = response!; - expect(definedResponse.status()).toBe(200); - expect(definedResponse.url()).toContain("/subpath/capture-the-flag"); - }); - - it("subpath/test-3 should be 404", async () => { - const response = await page.goto("http://localhost:5050/subpath/test-3"); - expect(response).toBeDefined(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const definedResponse = response!; - expect(definedResponse.status()).toBe(404); - }); - - it("sitemap.xml should match snapshot", async () => { - const response = await page.goto("http://localhost:5050/subpath/sitemap.xml"); - expect(response).toBeDefined(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const definedResponse = response!; - expect(definedResponse.status()).toBe(200); - expect(await definedResponse.text()).toMatchSnapshot(); - }); - - it("revalidate-all/v3 all should work", async () => { - const response = await page.goto("http://localhost:5050/subpath/api/fern-docs/revalidate-all/v3", { - timeout: 10_000, - }); - expect(response).toBeDefined(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const definedResponse = response!; - expect(definedResponse.status()).toBe(200); - - const results = await definedResponse.json(); - - expect(results.successfulRevalidations).toHaveLength(2); - expect(results.failedRevalidations).toHaveLength(0); - }, 10_000); - - it("revalidate-all/v3 should work with trailing slash", async () => { - const response = await page.goto("http://localhost:5050/subpath/api/fern-docs/revalidate-all/v3/", { - timeout: 10_000, - }); - expect(response).toBeDefined(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const definedResponse = response!; - expect(definedResponse.status()).toBe(200); - - const results = await definedResponse.json(); - - expect(results.successfulRevalidations).toHaveLength(2); - expect(results.failedRevalidations).toHaveLength(0); - }, 10_000); - - it("revalidate-all/v4 should work", async () => { - const response = await page.goto("http://localhost:5050/subpath/api/fern-docs/revalidate-all/v4", { - timeout: 10_000, - }); - expect(response).toBeDefined(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const definedResponse = response!; - expect(definedResponse.status()).toBe(200); - - const results = await definedResponse.json(); - - expect(results.total).toBe(2); - expect(results.results).toHaveLength(2); - expect((results.results as object[]).map((r) => "success" in r && r.success)).toEqual([true, true]); - }, 10_000); - - it("revalidate-all/v4 should work with trailing slash", async () => { - const response = await page.goto("http://localhost:5050/subpath/api/fern-docs/revalidate-all/v4/", { - timeout: 10_000, - }); - expect(response).toBeDefined(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const definedResponse = response!; - expect(definedResponse.status()).toBe(200); - - const results = await definedResponse.json(); - - expect(results.total).toBe(2); - expect(results.results).toHaveLength(2); - expect((results.results as object[]).map((r) => "success" in r && r.success)).toEqual([true, true]); - }); -}, 10_000); - -function exec(command: string | string[], opts?: ExecSyncOptions): string { - const cmd = (Array.isArray(command) ? command : command.split(" ")).filter((c) => c.trim().length > 0); - - if (!cmd[0]) { - throw new Error("Empty command"); - } - - try { - return String( - execFileSync(cmd[0], cmd.slice(1), { - stdio: ["inherit", "pipe", "pipe"], - ...opts, - env: { ...process.env, ...opts?.env }, - }), - ); - } catch (e) { - throw String(e); - } -} diff --git a/test/docs-e2e/skew-protection/index.test.ts b/test/docs-e2e/skew-protection/index.test.ts deleted file mode 100644 index 9408f765f8..0000000000 --- a/test/docs-e2e/skew-protection/index.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { chromium, Request } from "playwright"; -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -import { getDeploymentOrigin } from "../../utils/deployment-url"; - -const origin = getDeploymentOrigin(); - -describe("skew-protection", () => { - let browser: Awaited>; - - beforeAll(async () => { - browser = await chromium.launch(); - }); - - afterAll(async () => { - await browser?.close(); - }); - - let page: Awaited>; - beforeEach(async () => { - page = await browser.newPage(); - await page.context().addCookies([ - { - name: "_fern_docs_preview", - value: "buildwithfern.com", - domain: origin, - path: "/", - }, - ]); - }); - - afterEach(async () => { - await page.close(); - }); - - it("should contain ?dpl= or x-deployment-id header on all scripts and prefetch requests", async () => { - const requests: Request[] = []; - page.on("request", async (request) => { - requests.push(request); - }); - - await page.goto(`${origin}/learn/home`); - - const scripts = await page.locator("script").all(); - expect(scripts.length).toBeGreaterThan(0); - - let nextScripts = 0; - let dpl: string | undefined = undefined; - for (const script of scripts) { - const src = await script.getAttribute("src"); - - if (src?.includes("/_next/")) { - nextScripts++; - expect(src).toContain("dpl=dpl_"); - if (dpl === undefined) { - dpl = src.split("dpl=")[1]; - } - } - } - - expect(nextScripts).toBeGreaterThan(0); - expect(dpl).not.toBeUndefined(); - - // Prefetch a page - await page.evaluate(async () => { - await window.next.router.prefetch("/learn/docs/getting-started/development"); - }); - - const allPrefetchHeaders = (await Promise.all(requests.map((req) => req.allHeaders()))).filter( - (headers) => headers["x-nextjs-data"] === "1", - ); - expect(allPrefetchHeaders.length).toBeGreaterThan(0); - expect(allPrefetchHeaders.every((headers) => headers["x-deployment-id"] === dpl)).toBe(true); - }); -}, 20_000); diff --git a/test/utils/deployment-url.ts b/test/utils/deployment-url.ts deleted file mode 100644 index 475fcdbcde..0000000000 --- a/test/utils/deployment-url.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function getDeploymentOrigin(): string { - let origin = process.env.DEPLOYMENT_URL ?? "https://app-staging.buildwithfern.com"; - if (!origin.startsWith("http")) { - origin = `${origin.includes("localhost") ? "http" : "https"}://${origin}`; - } - return origin; -}