diff --git a/app/views/shared/_navbar.html.erb b/app/views/shared/_navbar.html.erb
index 6d66674c..7c85676a 100644
--- a/app/views/shared/_navbar.html.erb
+++ b/app/views/shared/_navbar.html.erb
@@ -20,7 +20,12 @@
<% else %>
<%= render 'shared/navbar_buttons' %>
- <%= link_to t('settings'), settings_profile_index_path, class: 'hidden md:inline-flex font-medium text-lg', id: 'account_settings_button' %>
+
+ <%= link_to t('settings'), settings_profile_index_path, class: 'inline-flex font-medium text-lg', id: 'account_settings_button' %>
+ <% if ENV['APP_VERSION'].present? %>
+ <%= ENV['APP_VERSION'] %>
+ <% end %>
+
<% end %>
diff --git a/playwright/.gitignore b/playwright/.gitignore
new file mode 100644
index 00000000..1f95add8
--- /dev/null
+++ b/playwright/.gitignore
@@ -0,0 +1,5 @@
+node_modules/
+test-results/
+playwright-report/
+blob-report/
+playwright/.cache/
diff --git a/playwright/README.md b/playwright/README.md
new file mode 100644
index 00000000..54726bd6
--- /dev/null
+++ b/playwright/README.md
@@ -0,0 +1,39 @@
+# DocuSeal Playwright Tests
+
+End-to-end tests for DocuSeal fork features.
+
+## Setup
+
+```bash
+cd playwright
+npm install
+npm run install:browsers
+```
+
+## Running
+
+```bash
+# Against local dev server (http://localhost:3000)
+npm test
+
+# Against UAT
+PLAYWRIGHT_BASE_URL=https://docuseal-uat.example.com npm test
+
+# Headed / debug UI
+npm run test:headed
+npm run test:ui
+```
+
+## Env vars
+
+- `PLAYWRIGHT_BASE_URL` — explicit target URL (overrides everything)
+- `DOCUSEAL_UAT_URL` — default UAT URL when `PLAYWRIGHT_BASE_URL` absent
+- `DOCUSEAL_ADMIN_EMAIL` / `DOCUSEAL_ADMIN_PASSWORD` — credentials for login helpers
+
+## Layout
+
+One spec per feature (mirrors `PLAN.md` version numbering):
+
+- `v0.4.0-version-display.spec.ts`
+- `v0.1.0-config-overrides.spec.ts`
+- ...
diff --git a/playwright/package.json b/playwright/package.json
new file mode 100644
index 00000000..d699339a
--- /dev/null
+++ b/playwright/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "docuseal-playwright",
+ "private": true,
+ "version": "0.0.0",
+ "description": "Playwright end-to-end tests for DocuSeal fork features",
+ "scripts": {
+ "test": "playwright test",
+ "test:headed": "playwright test --headed",
+ "test:ui": "playwright test --ui",
+ "install:browsers": "playwright install --with-deps chromium"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.47.0"
+ }
+}
diff --git a/playwright/playwright.config.ts b/playwright/playwright.config.ts
new file mode 100644
index 00000000..a3b14206
--- /dev/null
+++ b/playwright/playwright.config.ts
@@ -0,0 +1,29 @@
+import { defineConfig, devices } from '@playwright/test';
+
+// Base URL order: PLAYWRIGHT_BASE_URL > UAT default > localhost
+const baseURL =
+ process.env.PLAYWRIGHT_BASE_URL ||
+ process.env.DOCUSEAL_UAT_URL ||
+ 'http://localhost:3000';
+
+export default defineConfig({
+ testDir: './tests',
+ fullyParallel: false,
+ forbidOnly: !!process.env.CI,
+ retries: process.env.CI ? 1 : 0,
+ workers: 1,
+ reporter: [['list'], ['html', { open: 'never' }]],
+ use: {
+ baseURL,
+ trace: 'on-first-retry',
+ screenshot: 'only-on-failure',
+ video: 'retain-on-failure',
+ ignoreHTTPSErrors: true,
+ },
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ ],
+});
diff --git a/playwright/tests/helpers/auth.ts b/playwright/tests/helpers/auth.ts
new file mode 100644
index 00000000..da5707ab
--- /dev/null
+++ b/playwright/tests/helpers/auth.ts
@@ -0,0 +1,17 @@
+import { Page, expect } from '@playwright/test';
+
+// Credentials come from env so tests work against any environment.
+export const adminEmail = process.env.DOCUSEAL_ADMIN_EMAIL || 'admin@example.com';
+export const adminPassword = process.env.DOCUSEAL_ADMIN_PASSWORD || 'password';
+
+export async function loginAs(page: Page, email: string, password: string): Promise {
+ await page.goto('/users/sign_in');
+ await page.getByLabel(/email/i).fill(email);
+ await page.getByLabel(/password/i).fill(password);
+ await page.getByRole('button', { name: /sign in|log in/i }).click();
+ await expect(page).not.toHaveURL(/sign_in/);
+}
+
+export async function loginAsAdmin(page: Page): Promise {
+ await loginAs(page, adminEmail, adminPassword);
+}
diff --git a/playwright/tests/v0.4.0-version-display.spec.ts b/playwright/tests/v0.4.0-version-display.spec.ts
new file mode 100644
index 00000000..80bf4a2d
--- /dev/null
+++ b/playwright/tests/v0.4.0-version-display.spec.ts
@@ -0,0 +1,25 @@
+import { test, expect } from '@playwright/test';
+import { loginAsAdmin } from './helpers/auth';
+
+// Phase 0.4 — Version Display
+// Assumes the app is running with APP_VERSION=v0.4.0 (kustomize configmap in UAT).
+
+test.describe('Version display', () => {
+ test('navbar shows APP_VERSION below Settings link', async ({ page }) => {
+ await loginAsAdmin(page);
+ await page.goto('/');
+
+ const version = page.locator('#app_version');
+ await expect(version).toBeVisible();
+ await expect(version).toHaveText(/^v\d+\.\d+\.\d+/);
+ });
+
+ test('settings nav bottom version badge links to upstream releases', async ({ page }) => {
+ await loginAsAdmin(page);
+ await page.goto('/settings/profile');
+
+ const badge = page.locator('a[href="https://github.com/docusealco/docuseal/releases"]');
+ await expect(badge).toBeVisible();
+ await expect(badge).toHaveText(/^v\d+\.\d+\.\d+/);
+ });
+});