diff --git a/packages/e2e-react/README.md b/packages/e2e-react/README.md
index 181d94ad92..355a8f983b 100644
--- a/packages/e2e-react/README.md
+++ b/packages/e2e-react/README.md
@@ -1 +1,51 @@
-# E2E EmbeddedChat setup
+# E2E EmbeddedChat Tests
+
+End-to-end tests using Playwright for the EmbeddedChat React package. Tests focus on UI stability and deterministic flows without external service dependencies.
+
+## Setup
+
+### Install system dependencies (Linux)
+```bash
+sudo yarn playwright install-deps
+```
+
+### Install Playwright browsers
+```bash
+cd packages/e2e-react
+npx playwright install chromium
+```
+
+## Run Tests
+
+### Local
+```bash
+yarn workspace e2e-react test
+```
+
+### Watch mode
+```bash
+yarn workspace e2e-react test --watch
+```
+
+### Debug UI
+```bash
+yarn workspace e2e-react test --debug
+```
+
+### View HTML report
+```bash
+yarn workspace e2e-react show-report
+```
+
+## Test Coverage
+
+- **renders unauthenticated chat state** — Verifies EmbeddedChat loads with login prompt
+- **opens login modal from join button** — Ensures JOIN button opens password auth modal
+- **shows required field validation for empty login submit** — Tests form validation without auth service
+
+## Configuration
+
+- Base URL: `http://127.0.0.1:5173` (dev server)
+- Host (RC server): Configurable via `VITE_RC_HOST` env var, defaults to `http://127.0.0.1:3000`
+- CI retries: 2 (enabled on CI only)
+- Failure artifacts: Screenshots + traces automatically collected
diff --git a/packages/e2e-react/playwright.config.ts b/packages/e2e-react/playwright.config.ts
index a91deee96a..25f339254f 100644
--- a/packages/e2e-react/playwright.config.ts
+++ b/packages/e2e-react/playwright.config.ts
@@ -20,14 +20,17 @@ export default defineConfig({
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
- reporter: 'html',
+ reporter: process.env.CI
+ ? [['github'], ['html', { open: 'never' }]]
+ : [['list'], ['html', { open: 'never' }]],
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: 'http://127.0.0.1:5173',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
- trace: 'on-first-retry',
+ trace: 'retain-on-failure',
+ screenshot: 'only-on-failure',
},
/* Configure projects for major browsers */
diff --git a/packages/e2e-react/src/App.tsx b/packages/e2e-react/src/App.tsx
index d0f16785a3..bfe088fbd0 100644
--- a/packages/e2e-react/src/App.tsx
+++ b/packages/e2e-react/src/App.tsx
@@ -2,9 +2,11 @@
import { EmbeddedChat } from "@embeddedchat/react";
function App() {
+ const host = import.meta.env.VITE_RC_HOST || "http://127.0.0.1:3000";
+
return (
);
diff --git a/packages/e2e-react/src/vite-env.d.ts b/packages/e2e-react/src/vite-env.d.ts
index 11f02fe2a0..15758147fa 100644
--- a/packages/e2e-react/src/vite-env.d.ts
+++ b/packages/e2e-react/src/vite-env.d.ts
@@ -1 +1,9 @@
///
+
+interface ImportMetaEnv {
+ readonly VITE_RC_HOST?: string;
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv;
+}
diff --git a/packages/e2e-react/tests/example.spec.ts b/packages/e2e-react/tests/example.spec.ts
index 1189f4442a..9bbdc1cf20 100644
--- a/packages/e2e-react/tests/example.spec.ts
+++ b/packages/e2e-react/tests/example.spec.ts
@@ -1,13 +1,27 @@
import { test, expect } from "@playwright/test";
-test("EmbeddedChat should render", async ({ page }) => {
+test.beforeEach(async ({ page }) => {
await page.goto("/");
+});
+
+test("renders unauthenticated chat state", async ({ page }) => {
await expect(page.locator(".ec-embedded-chat")).toBeVisible();
+ await expect(page.getByRole("heading", { name: "Login to chat" })).toBeVisible();
+ await expect(page.getByRole("button", { name: "JOIN" })).toBeVisible();
+ await expect(page.getByPlaceholder("Sign in to chat")).toBeDisabled();
});
-test("EmbeddedChat has a title", async ({ page }) => {
- await page.goto("/");
- await expect(page.locator(".ec-chat-header--channelName")).toHaveText(
- "Login to chat"
- );
+test("opens login modal from join button", async ({ page }) => {
+ await page.getByRole("button", { name: "JOIN" }).click();
+
+ await expect(page.getByRole("heading", { name: "Login" })).toBeVisible();
+ await expect(page.getByText("Email or username")).toBeVisible();
+ await expect(page.getByText("Password")).toBeVisible();
+});
+
+test("shows required field validation for empty login submit", async ({ page }) => {
+ await page.getByRole("button", { name: "JOIN" }).click();
+ await page.getByRole("dialog").getByRole("button", { name: "Login" }).click();
+
+ await expect(page.getByText("This field is required")).toHaveCount(2);
});
diff --git a/packages/react/src/views/LoginForm/LoginForm.js b/packages/react/src/views/LoginForm/LoginForm.js
index 1285e5e1eb..53a8001315 100644
--- a/packages/react/src/views/LoginForm/LoginForm.js
+++ b/packages/react/src/views/LoginForm/LoginForm.js
@@ -41,8 +41,14 @@ export default function LoginForm() {
}, [userOrEmail, password]);
const handleSubmit = () => {
- if (!userOrEmail) setUserOrEmail('');
- if (!password) setPassword('');
+ if (!userOrEmail) {
+ setUserOrEmail('');
+ return;
+ }
+ if (!password) {
+ setPassword('');
+ return;
+ }
handleLogin(userOrEmail, password);
};
const handleClose = () => {