diff --git a/e2e/package-lock.json b/e2e/package-lock.json
new file mode 100644
index 0000000..90012fc
--- /dev/null
+++ b/e2e/package-lock.json
@@ -0,0 +1,111 @@
+{
+ "name": "chattyness-e2e",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "chattyness-e2e",
+ "version": "1.0.0",
+ "devDependencies": {
+ "@playwright/test": "^1.49.0",
+ "@types/node": "^22.0.0",
+ "typescript": "^5.7.0"
+ }
+ },
+ "node_modules/@playwright/test": {
+ "version": "1.57.0",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz",
+ "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright": "1.57.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "22.19.5",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.5.tgz",
+ "integrity": "sha512-HfF8+mYcHPcPypui3w3mvzuIErlNOh2OAG+BCeBZCEwyiD5ls2SiCwEyT47OELtf7M3nHxBdu0FsmzdKxkN52Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/playwright": {
+ "version": "1.57.0",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz",
+ "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.57.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.57.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz",
+ "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ }
+ }
+}
diff --git a/e2e/package.json b/e2e/package.json
new file mode 100644
index 0000000..29d1f3b
--- /dev/null
+++ b/e2e/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "chattyness-e2e",
+ "version": "1.0.0",
+ "description": "End-to-end tests for Chattyness",
+ "scripts": {
+ "test": "npx playwright test",
+ "test:ui": "npx playwright test --ui",
+ "test:headed": "npx playwright test --headed",
+ "test:debug": "npx playwright test --debug",
+ "report": "npx playwright show-report"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.49.0",
+ "@types/node": "^22.0.0",
+ "typescript": "^5.7.0"
+ }
+}
diff --git a/e2e/playwright-report/index.html b/e2e/playwright-report/index.html
new file mode 100644
index 0000000..db7767c
--- /dev/null
+++ b/e2e/playwright-report/index.html
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+ Playwright Test Report
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts
new file mode 100644
index 0000000..26c8085
--- /dev/null
+++ b/e2e/playwright.config.ts
@@ -0,0 +1,25 @@
+import { defineConfig, devices } from '@playwright/test';
+
+export default defineConfig({
+ testDir: './tests',
+ fullyParallel: false, // Run tests serially to maintain state between tests
+ forbidOnly: !!process.env.CI,
+ retries: process.env.CI ? 2 : 0,
+ workers: 1, // Single worker to maintain state
+ reporter: 'html',
+
+ use: {
+ trace: 'on-first-retry',
+ screenshot: 'only-on-failure',
+ video: 'retain-on-failure',
+ },
+
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ ],
+
+ outputDir: 'test-results/',
+});
diff --git a/e2e/test-results/.last-run.json b/e2e/test-results/.last-run.json
new file mode 100644
index 0000000..461eeba
--- /dev/null
+++ b/e2e/test-results/.last-run.json
@@ -0,0 +1,4 @@
+{
+ "status": "passed",
+ "failedTests": []
+}
\ No newline at end of file
diff --git a/e2e/test-results/ranosh-patio-daytime-background-admin.png b/e2e/test-results/ranosh-patio-daytime-background-admin.png
new file mode 100644
index 0000000..317d6e7
Binary files /dev/null and b/e2e/test-results/ranosh-patio-daytime-background-admin.png differ
diff --git a/e2e/test-results/ranosh-realm-success.png b/e2e/test-results/ranosh-realm-success.png
new file mode 100644
index 0000000..cbf7ca9
Binary files /dev/null and b/e2e/test-results/ranosh-realm-success.png differ
diff --git a/e2e/tests/ranosh-realm.spec.ts b/e2e/tests/ranosh-realm.spec.ts
new file mode 100644
index 0000000..1235587
--- /dev/null
+++ b/e2e/tests/ranosh-realm.spec.ts
@@ -0,0 +1,283 @@
+import { test, expect, APIRequestContext } from '@playwright/test';
+
+// Shared state between tests
+let ranoshTemporaryPassword: string;
+let ranoshRealmId: string;
+
+const ADMIN_BASE_URL = 'http://localhost:3001';
+const USER_BASE_URL = 'http://localhost:3000';
+
+test.describe.serial('Ranosh Realm E2E Setup', () => {
+
+ test('Step 1: Create Ranosh realm with ranosh user via Admin API', async ({ request }) => {
+ const response = await request.post(`${ADMIN_BASE_URL}/api/admin/realms`, {
+ data: {
+ name: 'Ranosh',
+ slug: 'ranosh',
+ description: 'Ranosh test realm',
+ privacy: 'public',
+ is_nsfw: false,
+ max_users: 100,
+ allow_guest_access: true,
+ new_owner: {
+ username: 'ranosh',
+ email: 'ranosh@example.com',
+ display_name: 'Ranosh',
+ },
+ },
+ });
+
+ expect(response.ok()).toBeTruthy();
+ const body = await response.json();
+
+ // Capture the temporary password for later use
+ expect(body.owner_temporary_password).toBeDefined();
+ expect(body.realm_id).toBeDefined();
+ expect(body.slug).toBe('ranosh');
+
+ ranoshTemporaryPassword = body.owner_temporary_password;
+ ranoshRealmId = body.realm_id;
+
+ console.log(`Created realm: ${body.slug}`);
+ console.log(`Owner temporary password captured`);
+ });
+
+ test('Step 2: Create Ranosh Patio Daytime scene via Admin API', async ({ request }) => {
+ const response = await request.post(`${ADMIN_BASE_URL}/api/admin/realms/ranosh/scenes`, {
+ data: {
+ name: 'Ranosh Patio Daytime',
+ slug: 'ranosh-patio-daytime',
+ description: 'The main entry scene for Ranosh realm during the day',
+ background_image_url: 'https://upload.wikimedia.org/wikipedia/commons/2/29/Ranosh_%2840577%29.jpg',
+ infer_dimensions_from_image: true,
+ is_entry_point: true,
+ is_hidden: false,
+ },
+ });
+
+ expect(response.ok()).toBeTruthy();
+ const body = await response.json();
+
+ expect(body.slug).toBe('ranosh-patio-daytime');
+ console.log(`Created scene: ${body.slug} (id: ${body.id})`);
+
+ // Verify the scene has a background image by fetching scene details
+ const sceneResponse = await request.get(`${ADMIN_BASE_URL}/api/admin/scenes/${body.id}`);
+ expect(sceneResponse.ok()).toBeTruthy();
+ const sceneDetails = await sceneResponse.json();
+
+ // Verify background image path is set
+ expect(sceneDetails.background_image_path).toBeTruthy();
+ expect(sceneDetails.background_image_path).toContain('/static/realm/');
+ console.log(`Background image path: ${sceneDetails.background_image_path}`);
+
+ // Verify dimensions were inferred from the image (not default 800x600)
+ expect(sceneDetails.bounds_wkt).toBeTruthy();
+ console.log(`Scene bounds: ${sceneDetails.bounds_wkt}`);
+ });
+
+ test('Step 2b: Create Ranosh Patio Nighttime scene via Admin API', async ({ request }) => {
+ const response = await request.post(`${ADMIN_BASE_URL}/api/admin/realms/ranosh/scenes`, {
+ data: {
+ name: 'Ranosh Patio Nighttime',
+ slug: 'ranosh-patio-nighttime',
+ description: 'The patio scene for Ranosh realm at night',
+ background_image_url: 'https://upload.wikimedia.org/wikipedia/commons/f/f3/Ranosh_%2816019%29.jpg',
+ infer_dimensions_from_image: true,
+ is_entry_point: false,
+ is_hidden: false,
+ },
+ });
+
+ expect(response.ok()).toBeTruthy();
+ const body = await response.json();
+
+ expect(body.slug).toBe('ranosh-patio-nighttime');
+ console.log(`Created scene: ${body.slug} (id: ${body.id})`);
+
+ // Verify the scene has a background image by fetching scene details
+ const sceneResponse = await request.get(`${ADMIN_BASE_URL}/api/admin/scenes/${body.id}`);
+ expect(sceneResponse.ok()).toBeTruthy();
+ const sceneDetails = await sceneResponse.json();
+
+ // Verify background image path is set
+ expect(sceneDetails.background_image_path).toBeTruthy();
+ expect(sceneDetails.background_image_path).toContain('/static/realm/');
+ console.log(`Background image path: ${sceneDetails.background_image_path}`);
+
+ // Verify dimensions were inferred from the image
+ expect(sceneDetails.bounds_wkt).toBeTruthy();
+ console.log(`Scene bounds: ${sceneDetails.bounds_wkt}`);
+ });
+
+ test('Step 2c: Create The Smoking Room scene via Admin API', async ({ request }) => {
+ const response = await request.post(`${ADMIN_BASE_URL}/api/admin/realms/ranosh/scenes`, {
+ data: {
+ name: 'The Smoking Room',
+ slug: 'the-smoking-room',
+ description: 'A cozy smoking room in Ranosh realm',
+ background_image_url: 'https://upload.wikimedia.org/wikipedia/commons/f/f8/Ranosh_%2814682%29.jpg',
+ infer_dimensions_from_image: true,
+ is_entry_point: false,
+ is_hidden: false,
+ },
+ });
+
+ expect(response.ok()).toBeTruthy();
+ const body = await response.json();
+
+ expect(body.slug).toBe('the-smoking-room');
+ console.log(`Created scene: ${body.slug} (id: ${body.id})`);
+
+ // Verify the scene has a background image by fetching scene details
+ const sceneResponse = await request.get(`${ADMIN_BASE_URL}/api/admin/scenes/${body.id}`);
+ expect(sceneResponse.ok()).toBeTruthy();
+ const sceneDetails = await sceneResponse.json();
+
+ // Verify background image path is set
+ expect(sceneDetails.background_image_path).toBeTruthy();
+ expect(sceneDetails.background_image_path).toContain('/static/realm/');
+ console.log(`Background image path: ${sceneDetails.background_image_path}`);
+
+ // Verify dimensions were inferred from the image
+ expect(sceneDetails.bounds_wkt).toBeTruthy();
+ console.log(`Scene bounds: ${sceneDetails.bounds_wkt}`);
+ });
+
+ test('Step 2d: Create The Corridor scene via Admin API', async ({ request }) => {
+ const response = await request.post(`${ADMIN_BASE_URL}/api/admin/realms/ranosh/scenes`, {
+ data: {
+ name: 'The Corridor',
+ slug: 'the-corridor',
+ description: 'A corridor in Ranosh realm',
+ background_image_url: 'https://upload.wikimedia.org/wikipedia/commons/4/48/Ranosh_%2843389%29.jpg',
+ infer_dimensions_from_image: true,
+ is_entry_point: false,
+ is_hidden: false,
+ },
+ });
+
+ expect(response.ok()).toBeTruthy();
+ const body = await response.json();
+
+ expect(body.slug).toBe('the-corridor');
+ console.log(`Created scene: ${body.slug} (id: ${body.id})`);
+
+ // Verify the scene has a background image by fetching scene details
+ const sceneResponse = await request.get(`${ADMIN_BASE_URL}/api/admin/scenes/${body.id}`);
+ expect(sceneResponse.ok()).toBeTruthy();
+ const sceneDetails = await sceneResponse.json();
+
+ // Verify background image path is set
+ expect(sceneDetails.background_image_path).toBeTruthy();
+ expect(sceneDetails.background_image_path).toContain('/static/realm/');
+ console.log(`Background image path: ${sceneDetails.background_image_path}`);
+
+ // Verify dimensions were inferred from the image
+ expect(sceneDetails.bounds_wkt).toBeTruthy();
+ console.log(`Scene bounds: ${sceneDetails.bounds_wkt}`);
+ });
+
+ test('Step 2e: Create Ranosh Family Room scene via Admin API', async ({ request }) => {
+ const response = await request.post(`${ADMIN_BASE_URL}/api/admin/realms/ranosh/scenes`, {
+ data: {
+ name: 'Ranosh Family Room',
+ slug: 'ranosh-family-room',
+ description: 'The family room in Ranosh realm',
+ background_image_url: 'https://upload.wikimedia.org/wikipedia/commons/f/fd/Ranosh_%2815637%29.jpg',
+ infer_dimensions_from_image: true,
+ is_entry_point: false,
+ is_hidden: false,
+ },
+ });
+
+ expect(response.ok()).toBeTruthy();
+ const body = await response.json();
+
+ expect(body.slug).toBe('ranosh-family-room');
+ console.log(`Created scene: ${body.slug} (id: ${body.id})`);
+
+ // Verify the scene has a background image by fetching scene details
+ const sceneResponse = await request.get(`${ADMIN_BASE_URL}/api/admin/scenes/${body.id}`);
+ expect(sceneResponse.ok()).toBeTruthy();
+ const sceneDetails = await sceneResponse.json();
+
+ // Verify background image path is set
+ expect(sceneDetails.background_image_path).toBeTruthy();
+ expect(sceneDetails.background_image_path).toContain('/static/realm/');
+ console.log(`Background image path: ${sceneDetails.background_image_path}`);
+
+ // Verify dimensions were inferred from the image
+ expect(sceneDetails.bounds_wkt).toBeTruthy();
+ console.log(`Scene bounds: ${sceneDetails.bounds_wkt}`);
+ });
+
+ test('Step 2f: Verify background in Admin UI', async ({ page }) => {
+ // Navigate to admin UI and verify background is shown
+ await page.goto(`${ADMIN_BASE_URL}/admin/realms/ranosh/scenes`);
+ await page.waitForLoadState('networkidle');
+
+ // Click on Ranosh Patio Daytime scene (the entry point)
+ await page.getByRole('link', { name: 'Ranosh Patio Daytime' }).click();
+ await page.waitForLoadState('networkidle');
+
+ // Verify the background field is not "None"
+ const backgroundText = await page.locator('text=Background').locator('..').locator('text=None').count();
+ expect(backgroundText).toBe(0); // Should not find "None" next to Background
+
+ // Take a screenshot for verification
+ await page.screenshot({ path: 'test-results/ranosh-patio-daytime-background-admin.png' });
+ console.log('Admin UI screenshot saved');
+ });
+
+ test('Step 3: Login as ranosh to Ranosh realm on User App', async ({ page }) => {
+ // Ensure we have the password from step 1
+ expect(ranoshTemporaryPassword).toBeDefined();
+
+ // Navigate to user app login page
+ await page.goto(USER_BASE_URL);
+
+ // Wait for the page to load
+ await page.waitForLoadState('networkidle');
+
+ // Enter the private realm slug
+ const privateRealmInput = page.getByPlaceholder('Or enter a private realm name');
+ await privateRealmInput.fill('ranosh');
+
+ // Enter username
+ await page.locator('#username').fill('ranosh');
+
+ // Enter password (temporary password from realm creation)
+ await page.locator('#password').fill(ranoshTemporaryPassword);
+
+ // Click the login button
+ await page.getByRole('button', { name: 'Enter Realm' }).click();
+
+ // Wait for navigation - should redirect to password reset page
+ await page.waitForURL('**/password-reset', { timeout: 10000 });
+ expect(page.url()).toContain('/password-reset');
+
+ console.log('Redirected to password reset page');
+
+ // Fill in new password
+ await page.locator('#new-password').fill('ranoshranosh');
+ await page.locator('#confirm-password').fill('ranoshranosh');
+
+ // Submit password reset
+ await page.getByRole('button', { name: 'Reset Password' }).click();
+
+ // Wait for navigation to the realm page
+ await page.waitForURL('**/realms/ranosh', { timeout: 10000 });
+ expect(page.url()).toContain('/realms/ranosh');
+
+ console.log('Successfully logged in and redirected to realm page');
+
+ // Verify we're on the realm page
+ await expect(page).toHaveURL(/\/realms\/ranosh/);
+
+ // Take a screenshot for verification
+ await page.screenshot({ path: 'test-results/ranosh-realm-success.png' });
+ console.log('Screenshot saved to test-results/ranosh-realm-success.png');
+ });
+
+});
diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json
new file mode 100644
index 0000000..2e86c0b
--- /dev/null
+++ b/e2e/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "declaration": false,
+ "noEmit": true
+ },
+ "include": ["**/*.ts"],
+ "exclude": ["node_modules"]
+}