diff --git a/.gitignore b/.gitignore index 27ae031..9e5e559 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,10 @@ pnpm-debug.log* yarn-debug.log* yarn-error.log* +# Testing +test-results/ +playwright-report/ + # Deployment *.env .azure diff --git a/package-lock.json b/package-lock.json index 7cb7381..3f3278a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "packages/*" ], "devDependencies": { + "@playwright/test": "^1.54.1", "concurrently": "^9.0.0", "lint-staged": "^16.0.0", "prettier": "^3.0.3", @@ -2012,13 +2013,12 @@ } }, "node_modules/@playwright/test": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz", - "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==", + "version": "1.54.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.1.tgz", + "integrity": "sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "playwright": "1.52.0" + "playwright": "1.54.1" }, "bin": { "playwright": "cli.js" @@ -9593,13 +9593,12 @@ } }, "node_modules/playwright": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", - "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", + "version": "1.54.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.1.tgz", + "integrity": "sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "playwright-core": "1.52.0" + "playwright-core": "1.54.1" }, "bin": { "playwright": "cli.js" @@ -9612,11 +9611,10 @@ } }, "node_modules/playwright-core": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", - "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", + "version": "1.54.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.1.tgz", + "integrity": "sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==", "license": "Apache-2.0", - "peer": true, "bin": { "playwright-core": "cli.js" }, diff --git a/package.json b/package.json index 90012b5..89182dd 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,9 @@ "build:api": "npm run build --workspace=api", "clean": "npm run clean --workspaces --if-present", "upload:docs": "node scripts/upload-documents.js http://localhost:7071", + "test": "playwright test", + "test:headed": "playwright test --headed", + "test:ui": "playwright test --ui", "lint": "xo", "lint:fix": "xo --fix", "format": "prettier --list-different --write .", @@ -44,6 +47,7 @@ "packages/*" ], "devDependencies": { + "@playwright/test": "^1.54.1", "concurrently": "^9.0.0", "lint-staged": "^16.0.0", "prettier": "^3.0.3", @@ -69,6 +73,25 @@ "envs": [ "node" ], + "overrides": [ + { + "files": [ + "test/**/*.ts", + "test/**/*.js", + "playwright.config.ts" + ], + "rules": { + "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-argument": "off", + "@typescript-eslint/no-unsafe-return": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "no-implicit-coercion": "off", + "unicorn/numeric-separators-style": "off", + "no-await-in-loop": "off" + } + } + ], "rules": { "@typescript-eslint/triple-slash-reference": "off", "@typescript-eslint/naming-convention": "off", diff --git a/playwright-report/index.html b/playwright-report/index.html new file mode 100644 index 0000000..89623da --- /dev/null +++ b/playwright-report/index.html @@ -0,0 +1,16736 @@ + + + + + + + Playwright Test Report + + + + +
+ + + diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..083f083 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,78 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * @see https://playwright.dev/docs/test-configuration + */ +export default defineConfig({ + testDir: './test/e2e', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: Boolean(process.env.CI), + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* 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', + /* 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://localhost:8000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + webServer: [ + { + command: 'npm run start:webapp', + url: 'http://localhost:8000', + reuseExistingServer: !process.env.CI, + }, + { + command: 'npm run start:api', + url: 'http://localhost:7071', + reuseExistingServer: !process.env.CI, + }, + ], +}); diff --git a/test-results/.last-run.json b/test-results/.last-run.json new file mode 100644 index 0000000..5fca3f8 --- /dev/null +++ b/test-results/.last-run.json @@ -0,0 +1,4 @@ +{ + "status": "failed", + "failedTests": [] +} \ No newline at end of file diff --git a/test/e2e/.gitkeep b/test/e2e/.gitkeep new file mode 100644 index 0000000..c0fc906 --- /dev/null +++ b/test/e2e/.gitkeep @@ -0,0 +1 @@ +# E2E Tests Directory \ No newline at end of file diff --git a/test/e2e/README.md b/test/e2e/README.md new file mode 100644 index 0000000..41125d3 --- /dev/null +++ b/test/e2e/README.md @@ -0,0 +1,71 @@ +# E2E Testing with Playwright + +This directory contains end-to-end tests for the serverless chat application using Playwright. + +## Test Structure + +- `api-chat.spec.ts` - Tests for the chat streaming API endpoint +- `api-documents.spec.ts` - Tests for document upload and retrieval APIs +- `ui-chat.spec.ts` - Tests for the chat user interface +- `full-workflow.spec.ts` - Complete end-to-end workflow tests + +## Running Tests + +### Prerequisites + +Make sure you have the development servers running: + +```bash +# Terminal 1: Start the API (Azure Functions) +npm run start:api + +# Terminal 2: Start the webapp +npm run start:webapp +``` + +### Run Tests + +```bash +# Run all tests +npm test + +# Run tests in headed mode (see browser) +npm run test:headed + +# Run tests with UI mode +npm run test:ui + +# Install Playwright browsers (required first time) +npx playwright install +``` + +### Test Configuration + +The tests are configured to: + +- Run against `http://localhost:8000` (webapp) and `http://localhost:7071` (API) +- Use Chromium, Firefox, and WebKit browsers +- Generate HTML reports +- Automatically start dev servers before running tests + +## Test Coverage + +### API Tests + +- Document upload with PDF files +- Document retrieval by filename +- Chat streaming with proper session management +- Error handling for invalid requests + +### UI Tests + +- Chat interface display and interactions +- Message sending and receiving +- Session persistence +- Suggestion handling + +### End-to-End Workflows + +- Upload document → Chat about uploaded content +- Multi-document upload and retrieval +- Session continuity across multiple questions diff --git a/test/e2e/api-chat.spec.ts b/test/e2e/api-chat.spec.ts new file mode 100644 index 0000000..b8f6803 --- /dev/null +++ b/test/e2e/api-chat.spec.ts @@ -0,0 +1,127 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Chat API', () => { + const API_BASE_URL = 'http://localhost:7071/api'; + + test('should handle chat request and return streaming response', async ({ request }) => { + const chatRequest = { + messages: [ + { + role: 'user', + content: 'Hello, can you help me with information about rental properties?', + }, + ], + context: { + sessionId: 'test-session-' + Date.now(), + }, + }; + + const response = await request.post(`${API_BASE_URL}/chats/stream`, { + data: chatRequest, + headers: { + 'Content-Type': 'application/json', + }, + }); + + expect(response.status()).toBe(200); + expect(response.headers()['content-type']).toBe('application/x-ndjson'); + expect(response.headers()['transfer-encoding']).toBe('chunked'); + + // Read the streaming response + const responseText = await response.text(); + expect(responseText.length).toBeGreaterThan(0); + + // Parse NDJSON response lines + const lines = responseText.trim().split('\n'); + expect(lines.length).toBeGreaterThan(0); + + // Each line should be valid JSON + for (const line of lines) { + if (line.trim()) { + const chunk = JSON.parse(line); + expect(chunk).toHaveProperty('delta'); + expect(chunk.delta).toHaveProperty('content'); + expect(chunk.delta).toHaveProperty('role', 'assistant'); + expect(chunk).toHaveProperty('context'); + expect(chunk.context).toHaveProperty('sessionId'); + } + } + }); + + test('should handle empty messages array', async ({ request }) => { + const chatRequest = { + messages: [], + context: { + sessionId: 'test-session-empty', + }, + }; + + const response = await request.post(`${API_BASE_URL}/chats/stream`, { + data: chatRequest, + headers: { + 'Content-Type': 'application/json', + }, + }); + + expect(response.status()).toBe(400); + const responseBody = await response.json(); + expect(responseBody).toHaveProperty('error'); + expect(responseBody.error).toContain('messages'); + }); + + test('should handle missing message content', async ({ request }) => { + const chatRequest = { + messages: [ + { + role: 'user', + content: '', + }, + ], + context: { + sessionId: 'test-session-no-content', + }, + }; + + const response = await request.post(`${API_BASE_URL}/chats/stream`, { + data: chatRequest, + headers: { + 'Content-Type': 'application/json', + }, + }); + + expect(response.status()).toBe(400); + const responseBody = await response.json(); + expect(responseBody).toHaveProperty('error'); + expect(responseBody.error).toContain('messages'); + }); + + test('should generate session ID when not provided', async ({ request }) => { + const chatRequest = { + messages: [ + { + role: 'user', + content: 'Test message without session ID', + }, + ], + }; + + const response = await request.post(`${API_BASE_URL}/chats/stream`, { + data: chatRequest, + headers: { + 'Content-Type': 'application/json', + }, + }); + + expect(response.status()).toBe(200); + + const responseText = await response.text(); + const lines = responseText.trim().split('\n'); + + if (lines.length > 0) { + const firstChunk = JSON.parse(lines[0]); + expect(firstChunk.context).toHaveProperty('sessionId'); + expect(firstChunk.context.sessionId).toBeTruthy(); + expect(firstChunk.context.sessionId.length).toBeGreaterThan(0); + } + }); +}); diff --git a/test/e2e/api-documents.spec.ts b/test/e2e/api-documents.spec.ts new file mode 100644 index 0000000..168dc9f --- /dev/null +++ b/test/e2e/api-documents.spec.ts @@ -0,0 +1,133 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Documents API', () => { + const API_BASE_URL = 'http://localhost:7071/api'; + + test('should upload a PDF document successfully', async ({ request }) => { + // Create a small test PDF file content (this is a minimal PDF structure) + const testPdfContent = Buffer.from( + [ + '%PDF-1.4', + '1 0 obj', + '<<', + '/Type /Catalog', + '/Pages 2 0 R', + '>>', + 'endobj', + '2 0 obj', + '<<', + '/Type /Pages', + '/Kids [3 0 R]', + '/Count 1', + '>>', + 'endobj', + '3 0 obj', + '<<', + '/Type /Page', + '/Parent 2 0 R', + '/MediaBox [0 0 612 792]', + '>>', + 'endobj', + 'xref', + '0 4', + '0000000000 65535 f ', + '0000000009 00000 n ', + '0000000058 00000 n ', + '0000000115 00000 n ', + 'trailer', + '<<', + '/Size 4', + '/Root 1 0 R', + '>>', + 'startxref', + '174', + '%%EOF', + ].join('\n'), + ); + + // Upload the test PDF + const formData = new FormData(); + const file = new File([testPdfContent], 'test-document.pdf', { type: 'application/pdf' }); + formData.append('file', file); + + const response = await request.post(`${API_BASE_URL}/documents`, { + multipart: { + file: { + name: 'test-document.pdf', + mimeType: 'application/pdf', + buffer: testPdfContent, + }, + }, + }); + + expect(response.status()).toBe(200); + const responseBody = await response.json(); + expect(responseBody).toHaveProperty('message'); + expect(responseBody.message).toContain('uploaded successfully'); + }); + + test('should retrieve an uploaded document', async ({ request }) => { + // First upload a document + const testPdfContent = Buffer.from( + [ + '%PDF-1.4', + '1 0 obj', + '<<', + '/Type /Catalog', + '/Pages 2 0 R', + '>>', + 'endobj', + 'xref', + '0 2', + '0000000000 65535 f ', + '0000000009 00000 n ', + 'trailer', + '<<', + '/Size 2', + '/Root 1 0 R', + '>>', + 'startxref', + '58', + '%%EOF', + ].join('\n'), + ); + + const fileName = 'test-retrieve.pdf'; + + // Upload the document first + await request.post(`${API_BASE_URL}/documents`, { + multipart: { + file: { + name: fileName, + mimeType: 'application/pdf', + buffer: testPdfContent, + }, + }, + }); + + // Then try to retrieve it + const getResponse = await request.get(`${API_BASE_URL}/documents/${fileName}`); + + expect(getResponse.status()).toBe(200); + expect(getResponse.headers()['content-type']).toBe('application/pdf'); + + const retrievedContent = await getResponse.body(); + expect(retrievedContent.length).toBeGreaterThan(0); + }); + + test('should return 404 for non-existent document', async ({ request }) => { + const response = await request.get(`${API_BASE_URL}/documents/non-existent-file.pdf`); + expect(response.status()).toBe(404); + }); + + test('should handle invalid file upload', async ({ request }) => { + const response = await request.post(`${API_BASE_URL}/documents`, { + data: {}, + }); + + expect(response.status()).toBe(400); + const responseBody = await response.json(); + expect(responseBody).toHaveProperty('error'); + expect(responseBody.error).toContain('file'); + }); +}); diff --git a/test/e2e/full-workflow.spec.ts b/test/e2e/full-workflow.spec.ts new file mode 100644 index 0000000..d93ecea --- /dev/null +++ b/test/e2e/full-workflow.spec.ts @@ -0,0 +1,241 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Full E2E Workflow', () => { + const API_BASE_URL = 'http://localhost:7071/api'; + + test('should upload document and then chat about it', async ({ page, request }) => { + // Step 1: Upload a test document via API + const testPdfContent = Buffer.from( + [ + '%PDF-1.4', + '1 0 obj', + '<<', + '/Type /Catalog', + '/Pages 2 0 R', + '>>', + 'endobj', + '2 0 obj', + '<<', + '/Type /Pages', + '/Kids [3 0 R]', + '/Count 1', + '>>', + 'endobj', + '3 0 obj', + '<<', + '/Type /Page', + '/Parent 2 0 R', + '/MediaBox [0 0 612 792]', + '/Contents 4 0 R', + '>>', + 'endobj', + '4 0 obj', + '<<', + '/Length 44', + '>>', + 'stream', + 'BT', + '/F1 12 Tf', + '72 720 Td', + '(Rental Property Information) Tj', + 'ET', + 'endstream', + 'endobj', + 'xref', + '0 5', + '0000000000 65535 f ', + '0000000009 00000 n ', + '0000000058 00000 n ', + '0000000115 00000 n ', + '0000000179 00000 n ', + 'trailer', + '<<', + '/Size 5', + '/Root 1 0 R', + '>>', + 'startxref', + '267', + '%%EOF', + ].join('\n'), + ); + + const fileName = 'test-rental-info.pdf'; + + // Upload the document + const uploadResponse = await request.post(`${API_BASE_URL}/documents`, { + multipart: { + file: { + name: fileName, + mimeType: 'application/pdf', + buffer: testPdfContent, + }, + }, + }); + + expect(uploadResponse.status()).toBe(200); + const uploadResult = await uploadResponse.json(); + expect(uploadResult.message).toContain('uploaded successfully'); + + // Step 2: Wait a moment for document to be indexed + await page.waitForTimeout(3000); + + // Step 3: Navigate to chat UI + await page.goto('/'); + + // Wait for the chat component to load + await page.waitForTimeout(2000); + + // Step 4: Ask a question about the uploaded document + const chatInput = page.locator('azc-chat').locator('textarea'); + await expect(chatInput).toBeVisible({ timeout: 10_000 }); + + const question = 'What information do you have about rental properties?'; + await chatInput.fill(question); + + const sendButton = page.locator('azc-chat').locator('button[type="submit"]'); + await sendButton.click(); + + // Step 5: Verify the question appears + await expect(page.locator('azc-chat').locator('.messages')).toContainText(question, { timeout: 10_000 }); + + // Step 6: Wait for and verify response + // Look for loading indicator first + await expect(page.locator('azc-chat').locator('.loading')).toBeVisible({ timeout: 5000 }); + + // Wait for response to complete (loading indicator should disappear) + await expect(page.locator('azc-chat').locator('.loading')).not.toBeVisible({ timeout: 30_000 }); + + // Check that we got some response from the assistant + const assistantMessages = page.locator('azc-chat').locator('.message.assistant'); + await expect(assistantMessages).toHaveCount(1, { timeout: 10_000 }); + + // Step 7: Verify citation link works (if citations are present) + const citationLinks = page.locator('azc-chat').locator('a[href*="/api/documents/"]'); + if ((await citationLinks.count()) > 0) { + const citationHref = await citationLinks.first().getAttribute('href'); + expect(citationHref).toContain('/api/documents/'); + + // Test that citation link points to our uploaded document + if (citationHref) { + const citationResponse = await request.get(citationHref); + expect(citationResponse.status()).toBe(200); + expect(citationResponse.headers()['content-type']).toBe('application/pdf'); + } + } + }); + + test('should handle multiple document uploads and retrieve specific documents', async ({ request }) => { + const documents = [ + { + name: 'rental-policy.pdf', + content: 'Rental policy information about deposits and lease terms', + }, + { + name: 'maintenance-guide.pdf', + content: 'Maintenance guide for tenants and property management', + }, + ]; + + // Upload multiple documents + for (const document of documents) { + const testPdfContent = Buffer.from( + [ + '%PDF-1.4', + '1 0 obj', + '<<', + '/Type /Catalog', + '/Pages 2 0 R', + '>>', + 'endobj', + '2 0 obj', + '<<', + '/Type /Pages', + '/Kids [3 0 R]', + '/Count 1', + '>>', + 'endobj', + '3 0 obj', + '<<', + '/Type /Page', + '/Parent 2 0 R', + '/MediaBox [0 0 612 792]', + '/Contents 4 0 R', + '>>', + 'endobj', + '4 0 obj', + '<<', + `/Length ${document.content.length + 20}`, + '>>', + 'stream', + 'BT', + '/F1 12 Tf', + '72 720 Td', + `(${document.content}) Tj`, + 'ET', + 'endstream', + 'endobj', + 'xref', + '0 5', + '0000000000 65535 f ', + '0000000009 00000 n ', + '0000000058 00000 n ', + '0000000115 00000 n ', + '0000000179 00000 n ', + 'trailer', + '<<', + '/Size 5', + '/Root 1 0 R', + '>>', + 'startxref', + '300', + '%%EOF', + ].join('\n'), + ); + + const response = await request.post(`${API_BASE_URL}/documents`, { + multipart: { + file: { + name: document.name, + mimeType: 'application/pdf', + buffer: testPdfContent, + }, + }, + }); + + expect(response.status()).toBe(200); + } + + // Verify we can retrieve each document + for (const document of documents) { + const getResponse = await request.get(`${API_BASE_URL}/documents/${document.name}`); + expect(getResponse.status()).toBe(200); + expect(getResponse.headers()['content-type']).toBe('application/pdf'); + } + }); + + test('should maintain chat session across multiple questions', async ({ page }) => { + // Navigate to chat + await page.goto('/'); + await page.waitForTimeout(2000); + + const chatInput = page.locator('azc-chat').locator('textarea'); + const sendButton = page.locator('azc-chat').locator('button[type="submit"]'); + + // Send first question + await chatInput.fill('What are your rental policies?'); + await sendButton.click(); + + // Wait for response + await expect(page.locator('azc-chat').locator('.message.user')).toHaveCount(1, { timeout: 10_000 }); + await expect(page.locator('azc-chat').locator('.loading')).not.toBeVisible({ timeout: 30_000 }); + + // Send follow-up question + await chatInput.fill('What about maintenance requests?'); + await sendButton.click(); + + // Verify both questions are in chat history + await expect(page.locator('azc-chat').locator('.message.user')).toHaveCount(2, { timeout: 10_000 }); + await expect(page.locator('azc-chat').locator('.messages')).toContainText('rental policies'); + await expect(page.locator('azc-chat').locator('.messages')).toContainText('maintenance requests'); + }); +}); diff --git a/test/e2e/ui-chat.spec.ts b/test/e2e/ui-chat.spec.ts new file mode 100644 index 0000000..cd8bbc2 --- /dev/null +++ b/test/e2e/ui-chat.spec.ts @@ -0,0 +1,133 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Chat UI E2E Tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + }); + + test('should display chat interface with initial elements', async ({ page }) => { + // Check for main navigation + await expect(page.locator('nav')).toContainText('AI Chat with Enterprise Data'); + + // Check for chat components + await expect(page.locator('azc-chat')).toBeVisible(); + await expect(page.locator('azc-history')).toBeVisible(); + + // Check for chat input + await expect(page.locator('azc-chat').locator('.chat-input-container')).toBeVisible(); + }); + + test('should display default prompt suggestions', async ({ page }) => { + // Wait for the component to load + await page.waitForTimeout(1000); + + // Check for prompt suggestions + const suggestions = page.locator('azc-chat').locator('.suggestion'); + + // Should have some default suggestions + await expect(suggestions.first()).toBeVisible({ timeout: 10_000 }); + + // Check for expected suggestion content + await expect(page.locator('azc-chat')).toContainText('Ask anything or try an example'); + }); + + test('should allow user to type and send a message', async ({ page }) => { + // Wait for component to be ready + await page.waitForTimeout(1000); + + // Find the chat input + const chatInput = page.locator('azc-chat').locator('textarea'); + await expect(chatInput).toBeVisible({ timeout: 10_000 }); + + // Type a message + const testMessage = 'Hello, can you help me with rental information?'; + await chatInput.fill(testMessage); + + // Send the message + const sendButton = page.locator('azc-chat').locator('button[type="submit"]'); + await sendButton.click(); + + // Check that the message appears in the chat + await expect(page.locator('azc-chat').locator('.messages')).toContainText(testMessage, { timeout: 10_000 }); + + // Check for loading indicator + await expect(page.locator('azc-chat').locator('.loading')).toBeVisible({ timeout: 5000 }); + }); + + test('should handle suggestion click', async ({ page }) => { + // Wait for component to load + await page.waitForTimeout(1000); + + // Find and click the first suggestion + const firstSuggestion = page.locator('azc-chat').locator('.suggestion').first(); + await expect(firstSuggestion).toBeVisible({ timeout: 10_000 }); + + const suggestionText = await firstSuggestion.textContent(); + await firstSuggestion.click(); + + // Verify the suggestion text appears in the chat + await expect(page.locator('azc-chat').locator('.messages')).toContainText(suggestionText || '', { + timeout: 10_000, + }); + }); + + test('should display chat history', async ({ page }) => { + // Check that history component exists + await expect(page.locator('azc-history')).toBeVisible(); + + // Send a message to create history + await page.waitForTimeout(1000); + const chatInput = page.locator('azc-chat').locator('textarea'); + await expect(chatInput).toBeVisible({ timeout: 10_000 }); + + await chatInput.fill('Test message for history'); + const sendButton = page.locator('azc-chat').locator('button[type="submit"]'); + await sendButton.click(); + + // Wait for response and check history updates + await page.waitForTimeout(3000); + + // History should show the conversation + await expect(page.locator('azc-history')).toBeVisible(); + }); + + test('should generate unique user ID and persist in localStorage', async ({ page }) => { + // Check that a user ID is generated and stored + const userId = await page.evaluate(() => localStorage.getItem('userId')); + expect(userId).toBeTruthy(); + expect(userId).toMatch(/^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/i); + + // Refresh the page and check that the same user ID is used + await page.reload(); + const sameUserId = await page.evaluate(() => localStorage.getItem('userId')); + expect(sameUserId).toBe(userId); + }); + + test('should handle new chat functionality', async ({ page }) => { + // Send a message first + await page.waitForTimeout(1000); + const chatInput = page.locator('azc-chat').locator('textarea'); + await expect(chatInput).toBeVisible({ timeout: 10_000 }); + + await chatInput.fill('First message'); + const sendButton = page.locator('azc-chat').locator('button[type="submit"]'); + await sendButton.click(); + + // Wait for message to appear + await expect(page.locator('azc-chat').locator('.messages')).toContainText('First message', { timeout: 10_000 }); + + // Look for new chat button and click it + const newChatButton = page + .locator('azc-chat') + .locator('button') + .filter({ hasText: /new chat/i }); + if ((await newChatButton.count()) > 0) { + await newChatButton.click(); + + // Check that messages are cleared + await page.waitForTimeout(1000); + const messagesCount = await page.locator('azc-chat').locator('.message').count(); + expect(messagesCount).toBe(0); + } + }); +});