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"] +}