diff --git a/.github/workflows/docs-preview-smoke-test.yml b/.github/workflows/docs-preview-smoke-test.yml new file mode 100644 index 000000000000..4908305fe570 --- /dev/null +++ b/.github/workflows/docs-preview-smoke-test.yml @@ -0,0 +1,277 @@ +name: Docs Preview Smoke Test + +on: + push: + branches: + - main + paths: + - ".github/workflows/docs-preview-smoke-test.yml" + - "packages/cli/docs-preview/**" + - "packages/cli/cli/**" + - "smoke-test/**" + pull_request: + paths: + - ".github/workflows/docs-preview-smoke-test.yml" + - "packages/cli/docs-preview/**" + - "packages/cli/cli/**" + - "smoke-test/**" + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: "buildwithfern" + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Install dependencies + uses: ./.github/actions/install + + - name: Compile packages + run: pnpm compile + + - name: Build CLI (prod) + run: pnpm turbo run dist:cli:prod --filter=@fern-api/cli + + - name: Upload CLI artifact + uses: actions/upload-artifact@v6 + with: + name: cli-prod-bundle + path: packages/cli/cli/dist/prod/ + retention-days: 1 + + smoke-test: + needs: build + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + node-version: [20, 22, 24, 26] + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + permissions: + contents: read + + steps: + - name: Disable Windows Defender real-time monitoring + if: runner.os == 'Windows' + shell: powershell + run: Set-MpPreference -DisableRealtimeMonitoring $true + + - name: Check Node.js version availability + id: node-check + shell: bash + run: | + NODE_VERSION="${{ matrix.node-version }}" + HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://nodejs.org/dist/latest-v${NODE_VERSION}.x/") + if [ "$HTTP_STATUS" = "200" ]; then + echo "available=true" >> $GITHUB_OUTPUT + echo "Node.js v${NODE_VERSION} is available" + else + echo "available=false" >> $GITHUB_OUTPUT + echo "Node.js v${NODE_VERSION} is not yet available (HTTP $HTTP_STATUS) — skipping" + fi + + - name: Checkout repository + if: steps.node-check.outputs.available == 'true' + uses: actions/checkout@v6 + + - name: Setup Node.js + if: steps.node-check.outputs.available == 'true' + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node-version }} + package-manager-cache: false + + - name: Setup Go for protoc-gen-openapi + if: steps.node-check.outputs.available == 'true' + uses: actions/setup-go@v6 + with: + go-version: "1.22" + + - name: Download CLI artifact + if: steps.node-check.outputs.available == 'true' + uses: actions/download-artifact@v7 + with: + name: cli-prod-bundle + path: ${{ runner.temp }}/cli-artifact + + - name: Install CLI native dependencies + if: steps.node-check.outputs.available == 'true' + shell: bash + run: | + cd "$RUNNER_TEMP/cli-artifact" + # The build resolves pnpm catalog: references to real versions. + # Run npm install in the isolated temp directory so npm doesn't + # walk up into the repo's pnpm workspace. + npm install --ignore-scripts + + - name: Install tools (Linux) + if: steps.node-check.outputs.available == 'true' && runner.os == 'Linux' + run: | + ( + cd smoke-test/playwright && npm install && npx playwright install chromium + echo "Playwright installed" + ) & + PW_PID=$! + + ( + sudo apt-get update && sudo apt-get install -y protobuf-compiler + BUF_VERSION=1.35.1 + curl -sSL "https://github.com/bufbuild/buf/releases/download/v${BUF_VERSION}/buf-Linux-x86_64.tar.gz" | sudo tar -xzC /usr/local --strip-components=1 + go install github.com/fern-api/protoc-gen-openapi/cmd/protoc-gen-openapi@v0.1.12 + echo "$(go env GOPATH)/bin" >> $GITHUB_PATH + echo "Proto toolchain installed" + ) & + PROTO_PID=$! + + wait $PW_PID + wait $PROTO_PID + + - name: Install tools (macOS) + if: steps.node-check.outputs.available == 'true' && runner.os == 'macOS' + run: | + ( + cd smoke-test/playwright && npm install && npx playwright install chromium + echo "Playwright installed" + ) & + PW_PID=$! + + ( + brew install protobuf buf + go install github.com/fern-api/protoc-gen-openapi/cmd/protoc-gen-openapi@v0.1.12 + echo "$(go env GOPATH)/bin" >> $GITHUB_PATH + echo "Proto toolchain installed" + ) & + PROTO_PID=$! + + wait $PW_PID + wait $PROTO_PID + + - name: Install tools (Windows) + if: steps.node-check.outputs.available == 'true' && runner.os == 'Windows' + shell: bash + run: | + ( + cd smoke-test/playwright && npm install && npx playwright install chromium + echo "Playwright installed" + ) & + PW_PID=$! + + ( + # Install protoc via direct download + PROTOC_VERSION=28.3 + curl -sSL -o "$RUNNER_TEMP/protoc.zip" "https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-win64.zip" + PROTOC_DIR="$RUNNER_TEMP/protoc" + mkdir -p "$PROTOC_DIR" + unzip -qo "$RUNNER_TEMP/protoc.zip" -d "$PROTOC_DIR" + echo "$PROTOC_DIR/bin" >> $GITHUB_PATH + # Install buf + BUF_VERSION=1.35.1 + TOOLS_DIR="$RUNNER_TEMP/tools" + mkdir -p "$TOOLS_DIR" + curl -sSL -o "$TOOLS_DIR/buf.exe" "https://github.com/bufbuild/buf/releases/download/v${BUF_VERSION}/buf-Windows-x86_64.exe" + echo "$TOOLS_DIR" >> $GITHUB_PATH + go install github.com/fern-api/protoc-gen-openapi/cmd/protoc-gen-openapi@v0.1.12 + echo "$(go env GOPATH)/bin" >> $GITHUB_PATH + echo "Proto toolchain installed" + ) & + PROTO_PID=$! + + wait $PW_PID + wait $PROTO_PID + + - name: Re-enable Windows Defender real-time monitoring + if: runner.os == 'Windows' + shell: powershell + run: Set-MpPreference -DisableRealtimeMonitoring $false + + - name: Start fern docs dev server + if: steps.node-check.outputs.available == 'true' + shell: bash + env: + FERN_TOKEN: ${{ secrets.FERN_TOKEN }} + run: | + cd smoke-test/fern + + # Use the locally-built CLI instead of the published npm version. + # The CLI will download the production docs bundle from S3 automatically. + FERN_NO_VERSION_REDIRECTION=true node "$RUNNER_TEMP/cli-artifact/cli.cjs" docs dev > /tmp/fern-docs-dev.log 2>&1 & + FERN_PID=$! + echo "FERN_PID=$FERN_PID" >> $GITHUB_ENV + + # Windows bundle installation (download + pnpm install) can take 3-5 minutes + TIMEOUT=300 + echo "Waiting for fern docs dev server to start (PID: $FERN_PID, timeout: ${TIMEOUT}s)..." + for i in $(seq 1 $TIMEOUT); do + if grep -q "Docs preview server ready" /tmp/fern-docs-dev.log 2>/dev/null; then + echo "Server is ready after ${i}s!" + echo "--- Dev server logs ---" + cat /tmp/fern-docs-dev.log + echo "--- End dev server logs ---" + break + fi + + if ! kill -0 $FERN_PID 2>/dev/null; then + echo "fern docs dev process (PID: $FERN_PID) exited unexpectedly" + echo "--- Dev server logs ---" + cat /tmp/fern-docs-dev.log + echo "--- End dev server logs ---" + exit 1 + fi + + if [ $i -eq $TIMEOUT ]; then + echo "Timeout waiting for server to start after ${TIMEOUT}s" + echo "--- Dev server logs ---" + cat /tmp/fern-docs-dev.log + echo "--- End dev server logs ---" + exit 1 + fi + + if [ $((i % 10)) -eq 0 ]; then + echo "--- Dev server logs at attempt $i ---" + tail -20 /tmp/fern-docs-dev.log + echo "--- End dev server logs ---" + fi + + sleep 1 + done + + - name: Run Playwright smoke tests + if: steps.node-check.outputs.available == 'true' + shell: bash + working-directory: smoke-test/playwright + run: npx playwright test smoke.spec.ts --config playwright.config.ts + + - name: Upload Playwright report + uses: actions/upload-artifact@v6 + if: always() && steps.node-check.outputs.available == 'true' + with: + name: smoke-test-playwright-report-${{ matrix.os }}-node${{ matrix.node-version }} + path: smoke-test/playwright/playwright-report/ + retention-days: 7 + + - name: Print fern docs dev logs + if: always() && steps.node-check.outputs.available == 'true' + shell: bash + run: cat /tmp/fern-docs-dev.log || true + + - name: Stop fern docs dev server + if: always() && steps.node-check.outputs.available == 'true' + shell: bash + run: | + if [ -n "$FERN_PID" ]; then + kill $FERN_PID 2>/dev/null || true + fi diff --git a/packages/cli/cli/build-utils.mjs b/packages/cli/cli/build-utils.mjs index 4557acf67453..9c5437be779f 100644 --- a/packages/cli/cli/build-utils.mjs +++ b/packages/cli/cli/build-utils.mjs @@ -1,4 +1,5 @@ import { exec } from "child_process"; +import { readFileSync } from "fs"; import { writeFile } from "fs/promises"; import path from "path"; import tsup from "tsup"; @@ -10,12 +11,59 @@ const execAsync = promisify(exec); const __dirname = path.dirname(fileURLToPath(import.meta.url)); +/** + * Parse the pnpm-workspace.yaml catalog section to resolve catalog: references. + * This is a lightweight parser that handles the simple `key: value` format used in the catalog. + */ +function loadPnpmCatalog() { + const workspaceRoot = path.resolve(__dirname, "../../.."); + const workspacePath = path.join(workspaceRoot, "pnpm-workspace.yaml"); + try { + const content = readFileSync(workspacePath, "utf-8"); + const catalog = {}; + let inCatalog = false; + for (const line of content.split("\n")) { + if (/^catalog:/.test(line)) { + inCatalog = true; + continue; + } + if (inCatalog) { + // Stop when we hit a non-indented line (new top-level key) + if (line.length > 0 && !line.startsWith(" ") && !line.startsWith("\t")) { + break; + } + const match = line.match(/^\s+["']?([^"':]+)["']?:\s*["']?([^"'\s]+)["']?/); + if (match) { + catalog[match[1]] = match[2]; + } + } + } + return catalog; + } catch (_error) { + // Gracefully degrade if workspace file is missing (e.g. running outside the monorepo). + // Catalog references will still fail explicitly in getDependencyVersion(). + return {}; + } +} + +const pnpmCatalog = loadPnpmCatalog(); + /** * Get a dependency version from package.json, preferring dependencies over devDependencies. - * This ensures we don't miss runtime dependencies regardless of where they're declared. + * Resolves pnpm catalog: references to actual version specifiers. */ function getDependencyVersion(packageName) { - return packageJson.dependencies?.[packageName] ?? packageJson.devDependencies?.[packageName]; + const version = packageJson.dependencies?.[packageName] ?? packageJson.devDependencies?.[packageName]; + if (version === "catalog:" || version === "catalog:default") { + const catalogVersion = pnpmCatalog[packageName]; + if (!catalogVersion) { + throw new Error( + `Dependency "${packageName}" uses catalog: but was not found in pnpm-workspace.yaml catalog` + ); + } + return catalogVersion; + } + return version; } /** diff --git a/packages/cli/docs-preview/src/downloadLocalDocsBundle.ts b/packages/cli/docs-preview/src/downloadLocalDocsBundle.ts index cbcca4b0b874..5fd178097816 100644 --- a/packages/cli/docs-preview/src/downloadLocalDocsBundle.ts +++ b/packages/cli/docs-preview/src/downloadLocalDocsBundle.ts @@ -106,6 +106,89 @@ const PNPMFILE_CJS_CONTENTS = `module.exports = { const NPMRC_CONTENTS = "@fern-fern:registry=https://npm.buildwithfern.com\n"; +// Marker file written after Windows post-processing completes so we can skip +// the (slow) pnpm install on subsequent cached-bundle runs. +const WINDOWS_POST_PROCESSED_MARKER = ".windows-post-processed"; + +function getPathToWindowsPostProcessedMarker({ app = false }: { app?: boolean }): AbsoluteFilePath { + return join(getPathToStandaloneFolder({ app }), RelativeFilePath.of(WINDOWS_POST_PROCESSED_MARKER)); +} + +/** + * Runs Windows-specific post-processing on the extracted bundle. + * The tar bundle contains Unix symlinks that don't work on Windows, so we + * write helper config files and run `pnpm install` in the standalone directory + * to recreate the missing node_modules entries. + * + * This is idempotent — it checks for existing files before writing and uses a + * marker file to skip the expensive `pnpm install` on subsequent runs. + */ +async function postProcessWindowsBundle({ app, logger }: { app: boolean; logger: Logger }): Promise { + const absPathToStandalone = getPathToStandaloneFolder({ app }); + if (!(await doesPathExist(absPathToStandalone))) { + logger.debug("Standalone folder does not exist, skipping Windows post-processing"); + return; + } + + // If the marker file exists, post-processing was already completed. + const markerPath = getPathToWindowsPostProcessedMarker({ app }); + if (await doesPathExist(markerPath)) { + logger.debug("Windows post-processing already completed (marker file exists), skipping"); + return; + } + + const absPathToInstrumentationJs = getPathToInstrumentationJs({ app }); + const pnpmWorkspacePath = getPathToPnpmWorkspaceYaml({ app }); + const pnpmfilePath = getPathToPnpmfileCjs({ app }); + const npmrcPath = getPathToNpmrc({ app }); + + // Check all paths in parallel + const [pnpmWorkspaceExists, pnpmfileExists, npmrcExists, instrumentationJsExists] = await Promise.all([ + doesPathExist(pnpmWorkspacePath), + doesPathExist(pnpmfilePath), + doesPathExist(npmrcPath), + doesPathExist(absPathToInstrumentationJs) + ]); + + // Warn if pnpm-workspace.yaml does not exist + if (!pnpmWorkspaceExists) { + logger.warn( + `Expected pnpm-workspace.yaml at ${pnpmWorkspacePath} but it does not exist. If you are experiencing issues, please contact support@buildwithfern.com.` + ); + } + + // Write pnpmfile.cjs if it does not exist + if (!pnpmfileExists) { + logger.debug(`Writing pnpmfile.cjs at ${pnpmfilePath}`); + await writeFile(pnpmfilePath, PNPMFILE_CJS_CONTENTS); + } + // Write .npmrc if it does not exist + if (!npmrcExists) { + logger.debug(`Writing .npmrc at ${npmrcPath}`); + await writeFile(npmrcPath, NPMRC_CONTENTS); + } + // Remove instrumentation.js if it exists + if (instrumentationJsExists) { + logger.debug(`Removing instrumentation.js at ${absPathToInstrumentationJs}`); + await rm(absPathToInstrumentationJs); + } + + try { + // pnpm install within standalone + logger.debug("Running pnpm install within standalone"); + await loggingExeca(logger, "pnpm", ["install"], { + cwd: absPathToStandalone, + doNotPipeOutput: true + }); + } catch (error) { + throw contactFernSupportError(`Failed to install required package due to error: ${error}`); + } + + // Write marker file so we skip this on future cached-bundle runs + await writeFile(markerPath, new Date().toISOString()); + logger.debug("Windows post-processing completed"); +} + export async function downloadBundle({ bucketUrl, logger, @@ -142,7 +225,12 @@ export async function downloadBundle({ } if (currentETag != null && currentETag === eTag) { logger.debug("ETag matches. Using already downloaded bundle"); - // The bundle is already downloaded + // The bundle is already downloaded, but on Windows we may still + // need to run post-processing (e.g. when the bundle was + // pre-deployed by CI or extracted externally). + if (PLATFORM_IS_WINDOWS && app) { + await postProcessWindowsBundle({ app, logger }); + } return { type: "success" }; @@ -265,7 +353,16 @@ export async function downloadBundle({ try { await decompress(outputZipPath, absolutePathToBundleFolder, { - filter: (file) => !(PLATFORM_IS_WINDOWS && file.type === "symlink") + filter: (file) => { + // On Windows, symlinks require admin privileges to create. + // The fern-platform bundle build now dereferences important + // symlinks at the source, so any remaining symlink entries + // are safe to skip on Windows. + if (PLATFORM_IS_WINDOWS && file.type === "symlink") { + return false; + } + return true; + } }); } finally { if (unzipInterval) { @@ -362,53 +459,7 @@ export async function downloadBundle({ } if (PLATFORM_IS_WINDOWS) { - const absPathToStandalone = getPathToStandaloneFolder({ app }); - const absPathToInstrumentationJs = getPathToInstrumentationJs({ app }); - const pnpmWorkspacePath = getPathToPnpmWorkspaceYaml({ app }); - const pnpmfilePath = getPathToPnpmfileCjs({ app }); - const npmrcPath = getPathToNpmrc({ app }); - - // Check all paths in parallel - const [pnpmWorkspaceExists, pnpmfileExists, npmrcExists, instrumentationJsExists] = await Promise.all([ - doesPathExist(pnpmWorkspacePath), - doesPathExist(pnpmfilePath), - doesPathExist(npmrcPath), - doesPathExist(absPathToInstrumentationJs) - ]); - - // Warn if pnpm-workspace.yaml does not exist - if (!pnpmWorkspaceExists) { - logger.warn( - `Expected pnpm-workspace.yaml at ${pnpmWorkspacePath} but it does not exist. If you are experiencing issues, please contact support@buildwithfern.com.` - ); - } - - // Write pnpmfile.cjs if it does not exist - if (!pnpmfileExists) { - logger.debug(`Writing pnpmfile.cjs at ${pnpmfilePath}`); - await writeFile(pnpmfilePath, PNPMFILE_CJS_CONTENTS); - } - // Write .npmrc if it does not exist - if (!npmrcExists) { - logger.debug(`Writing .npmrc at ${npmrcPath}`); - await writeFile(npmrcPath, NPMRC_CONTENTS); - } - // Remove instrumentation.js if it exists - if (instrumentationJsExists) { - logger.debug(`Removing instrumentation.js at ${absPathToInstrumentationJs}`); - await rm(absPathToInstrumentationJs); - } - - try { - // pnpm install within standalone - logger.debug("Running pnpm install within standalone"); - await loggingExeca(logger, "pnpm", ["install"], { - cwd: absPathToStandalone, - doNotPipeOutput: true - }); - } catch (error) { - throw contactFernSupportError(`Failed to install required package due to error: ${error}`); - } + await postProcessWindowsBundle({ app, logger }); } } diff --git a/smoke-test/fern/apis/rest-api/generators.yml b/smoke-test/fern/apis/rest-api/generators.yml new file mode 100644 index 000000000000..c2ffa4899f17 --- /dev/null +++ b/smoke-test/fern/apis/rest-api/generators.yml @@ -0,0 +1,5 @@ +# yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json + +api: + specs: + - openapi: openapi.yaml diff --git a/smoke-test/fern/apis/rest-api/openapi.yaml b/smoke-test/fern/apis/rest-api/openapi.yaml new file mode 100644 index 000000000000..651690a2c698 --- /dev/null +++ b/smoke-test/fern/apis/rest-api/openapi.yaml @@ -0,0 +1,76 @@ +openapi: 3.1.0 +info: + title: Smoke Test API + version: 1.0.0 + description: A minimal API for CLI smoke testing. +servers: + - url: https://api.example.com/v1 +paths: + /plants: + get: + summary: List plants + operationId: listPlants + responses: + "200": + description: A list of plants + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Plant" + post: + summary: Create a plant + operationId: createPlant + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Plant" + responses: + "201": + description: Plant created + content: + application/json: + schema: + $ref: "#/components/schemas/Plant" + /plants/{plantId}: + get: + summary: Get a plant by ID + operationId: getPlant + parameters: + - name: plantId + in: path + required: true + schema: + type: string + responses: + "200": + description: A plant + content: + application/json: + schema: + $ref: "#/components/schemas/Plant" + "404": + description: Plant not found +components: + schemas: + Plant: + type: object + required: + - id + - name + properties: + id: + type: string + name: + type: string + species: + type: string + status: + type: string + enum: + - available + - sold + - pending diff --git a/smoke-test/fern/docs.yml b/smoke-test/fern/docs.yml new file mode 100644 index 000000000000..92d7c4d58f6a --- /dev/null +++ b/smoke-test/fern/docs.yml @@ -0,0 +1,22 @@ +# yaml-language-server: $schema=https://schema.buildwithfern.dev/docs-yml.json + +instances: + - url: cli-smoke-test.docs.buildwithfern.com + +title: CLI Smoke Test + +layout: + searchbar-placement: header + +navigation: + - page: Welcome + path: ./docs/pages/welcome.mdx + - page: Getting Started + path: ./docs/pages/getting-started.mdx + - api: REST API + api-name: rest-api + +colors: + accentPrimary: + dark: "#81C784" + light: "#1B5E20" diff --git a/smoke-test/fern/docs/pages/getting-started.mdx b/smoke-test/fern/docs/pages/getting-started.mdx new file mode 100644 index 000000000000..efae33fa5623 --- /dev/null +++ b/smoke-test/fern/docs/pages/getting-started.mdx @@ -0,0 +1,15 @@ +--- +title: Getting Started +--- + +# Getting Started + +Follow these steps to get started with the smoke test API. + +## Step 1: Authentication + +Use your API key to authenticate requests. + +## Step 2: Make a request + +Send a GET request to the `/plants` endpoint. diff --git a/smoke-test/fern/docs/pages/welcome.mdx b/smoke-test/fern/docs/pages/welcome.mdx new file mode 100644 index 000000000000..0c5f01950007 --- /dev/null +++ b/smoke-test/fern/docs/pages/welcome.mdx @@ -0,0 +1,7 @@ +--- +title: Welcome +--- + +# Welcome + +This is a smoke test project for the Fern CLI docs preview. diff --git a/smoke-test/fern/fern.config.json b/smoke-test/fern/fern.config.json new file mode 100644 index 000000000000..e562ff42a3bc --- /dev/null +++ b/smoke-test/fern/fern.config.json @@ -0,0 +1,4 @@ +{ + "organization": "smoke-test", + "version": "*" +} diff --git a/smoke-test/playwright/package.json b/smoke-test/playwright/package.json new file mode 100644 index 000000000000..9eb834dde4c3 --- /dev/null +++ b/smoke-test/playwright/package.json @@ -0,0 +1,7 @@ +{ + "name": "smoke-test-playwright", + "private": true, + "dependencies": { + "@playwright/test": "^1.52.0" + } +} diff --git a/smoke-test/playwright/pages.ts b/smoke-test/playwright/pages.ts new file mode 100644 index 000000000000..92cadf8a5dbd --- /dev/null +++ b/smoke-test/playwright/pages.ts @@ -0,0 +1,11 @@ +/** + * Smoke test pages derived from the smoke-test docs.yml navigation. + * + * Each entry is a path that should return a 200 OK and render without + * uncaught page errors. Used by smoke.spec.ts. + */ +export const PAGES = [ + // Markdown pages + "/welcome", + "/getting-started" +]; diff --git a/smoke-test/playwright/playwright.config.ts b/smoke-test/playwright/playwright.config.ts new file mode 100644 index 000000000000..3432d86e07a6 --- /dev/null +++ b/smoke-test/playwright/playwright.config.ts @@ -0,0 +1,27 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + testDir: ".", + testMatch: "**/*.spec.ts", + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: 1, + timeout: 60_000, + reporter: process.env.CI + ? [["github"], ["html", { open: "never", outputFolder: "playwright-report" }], ["list"]] + : [["html", { open: "never", outputFolder: "playwright-report" }], ["list"]], + + use: { + baseURL: process.env.BASE_URL ?? "http://localhost:3000", + trace: "on-first-retry", + screenshot: "only-on-failure" + }, + + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] } + } + ] +}); diff --git a/smoke-test/playwright/smoke.spec.ts b/smoke-test/playwright/smoke.spec.ts new file mode 100644 index 000000000000..aaa4bf9c07bd --- /dev/null +++ b/smoke-test/playwright/smoke.spec.ts @@ -0,0 +1,26 @@ +import { expect, test } from "@playwright/test"; +import { PAGES } from "./pages"; + +const SMOKE_PAGES = [...PAGES, "/sitemap.xml"]; + +test.describe("Smoke test: all pages load", () => { + for (const pagePath of SMOKE_PAGES) { + test(`GET ${pagePath} returns 200`, async ({ page }) => { + const pageErrors: string[] = []; + page.on("pageerror", (error) => { + pageErrors.push(error.message); + }); + + const response = await page.goto(pagePath, { + waitUntil: "domcontentloaded", + timeout: 30_000 + }); + + expect(response, `Expected a response for ${pagePath}`).not.toBeNull(); + expect(response?.status(), `Expected 200 for ${pagePath} but got ${response?.status()}`).toBe(200); + + // Verify there were no uncaught page errors + expect(pageErrors, `Unexpected page errors on ${pagePath}: ${pageErrors.join(", ")}`).toHaveLength(0); + }); + } +});