diff --git a/GUI/.gitignore b/GUI/.gitignore index d79b5ca1..a49e18db 100644 --- a/GUI/.gitignore +++ b/GUI/.gitignore @@ -28,3 +28,8 @@ dist-ssr *.njsproj *.sln *.sw? + +# Playwright test artifacts +tests/screenshots/ +test-results/ +playwright-report/ \ No newline at end of file diff --git a/GUI/package-lock.json b/GUI/package-lock.json index 436ec9c4..bec486df 100644 --- a/GUI/package-lock.json +++ b/GUI/package-lock.json @@ -66,6 +66,7 @@ "zustand": "^4.4.4" }, "devDependencies": { + "@playwright/test": "^1.56.1", "@types/howler": "^2.2.11", "@types/lodash": "^4.14.191", "@types/lodash.debounce": "^4.0.7", @@ -4927,6 +4928,22 @@ "@parcel/core": "^2.12.0" } }, + "node_modules/@playwright/test": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", + "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.56.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -13008,6 +13025,53 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.56.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", + "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/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/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", diff --git a/GUI/package.json b/GUI/package.json index 09ab4a81..92613136 100644 --- a/GUI/package.json +++ b/GUI/package.json @@ -8,7 +8,14 @@ "build": "tsc && vite build", "preview": "vite preview", "lint": "tsc --noEmit && eslint \"./src/**/*.{js,ts,tsx}\"", - "prettier": "prettier --write \"{,!(node_modules)/**/}*.{ts,tsx,js,json,css,less,scss}\"" + "prettier": "prettier --write \"{,!(node_modules)/**/}*.{ts,tsx,js,json,css,less,scss}\"", + "test": "playwright test", + "test:ui": "playwright test --ui", + "test:headed": "playwright test --headed", + "test:debug": "playwright test --debug", + "test:chrome": "playwright test --project=chromium", + "test:chrome:headed": "playwright test --project=chromium --headed", + "test:chrome:debug": "playwright test --project=chromium --debug" }, "dependencies": { "@buerokratt-ria/styles": "^0.0.1", @@ -69,6 +76,7 @@ "zustand": "^4.4.4" }, "devDependencies": { + "@playwright/test": "^1.56.1", "@types/howler": "^2.2.11", "@types/lodash": "^4.14.191", "@types/lodash.debounce": "^4.0.7", diff --git a/GUI/playwright.config.ts b/GUI/playwright.config.ts new file mode 100644 index 00000000..e26a176a --- /dev/null +++ b/GUI/playwright.config.ts @@ -0,0 +1,54 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * @see https://playwright.dev/docs/test-configuration + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!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: { + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + + /* Take screenshot on failure */ + screenshot: 'only-on-failure', + + /* Record video on failure */ + video: 'retain-on-failure', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + ], + + /* Uncomment to run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run dev', + // url: 'http://localhost:3001', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/GUI/tests/README.md b/GUI/tests/README.md index 3a807f89..2c684398 100644 --- a/GUI/tests/README.md +++ b/GUI/tests/README.md @@ -1 +1,34 @@ -## Contains both unit and integration test for GUI \ No newline at end of file +## Contains both unit and integration test for GUI + +### Playwright E2E Tests + +This directory contains end-to-end tests using Playwright for the RAG Module GUI. + +#### Setup + +1. Install dependencies: + ```bash + npm install + ``` + +2. Install Playwright browsers: + ```bash + npx playwright install + ``` + +#### Running Tests + +- Run all tests: `npm test` +- Run tests with UI: `npm run test:ui` +- Run tests in headed mode: `npm run test:headed` +- Debug tests: `npm run test:debug` + +#### Test Files + +- `auth.spec.ts` - Authentication flow tests (requires auth service running) +- `basic-auth.spec.ts` - Basic authentication tests (can run standalone) + +#### Authentication Service + +The tests expect an authentication service running at `http://localhost:3004/et/dev-auth` (from .env.development). +If the service is not running, some tests will gracefully handle the failure and run alternative scenarios. \ No newline at end of file diff --git a/GUI/tests/helpers/llm-connections-helpers.ts b/GUI/tests/helpers/llm-connections-helpers.ts new file mode 100644 index 00000000..c0671680 --- /dev/null +++ b/GUI/tests/helpers/llm-connections-helpers.ts @@ -0,0 +1,404 @@ +import { Page } from '@playwright/test'; + +/** + * LLM Connections Helper + */ +export class LLMConnectionsHelper { + constructor(private page: Page) {} + + async navigateToLLMConnections(): Promise { + await this.page.goto('http://localhost:3003/rag-search/llm-connections'); + await this.page.waitForLoadState('networkidle'); + } + + async navigateToCreateConnection(): Promise { + // First navigate to LLM connections page + await this.navigateToLLMConnections(); + + // Look for create button + const createButton = this.page.locator('button').filter({ + hasText: /create.*connection|add.*connection|new.*connection/i + }); + + if (await createButton.count() > 0 && await createButton.isVisible()) { + await createButton.click(); + await this.page.waitForLoadState('networkidle'); + } else { + // Fallback to direct navigation + await this.page.goto('http://localhost:3003/rag-search/create-llm-connection'); + await this.page.waitForLoadState('networkidle'); + } + + // Wait for form to be visible and fully loaded + await this.page.locator('form').waitFor({ state: 'visible' }); + + // Wait for the LLM Configuration section to be visible + await this.page.locator('.form-section').filter({ hasText: /LLM Configuration/i }).waitFor({ state: 'visible' }); + + // Wait for connection name field to be ready + await this.page.locator('input[name="connectionName"]').waitFor({ state: 'visible' }); + } + + /** + * Fill connection name field + */ + async fillConnectionName(name: string): Promise { + const nameField = this.page.locator('input[name="connectionName"]'); + await nameField.waitFor({ state: 'visible' }); + await nameField.fill(name); + } + + /** + * Select LLM platform from dropdown + */ + async selectLLMPlatform(platformLabel: string): Promise { + const llmSection = this.page.locator('.form-section').filter({ hasText: /LLM Configuration/i }); + await llmSection.waitFor({ state: 'visible', timeout: 5000 }); + + const platformDropdown = llmSection.locator('.select').first(); + await platformDropdown.waitFor({ state: 'visible', timeout: 5000 }); + + const trigger = platformDropdown.locator('.select__trigger, .select-trigger, button').first(); + await trigger.waitFor({ state: 'visible', timeout: 5000 }); + await trigger.click(); + + // Scope options to the opened dropdown only + const options = platformDropdown.locator('.select__option, .select-option, .option'); + + const targetOption = options.filter({ hasText: new RegExp(platformLabel, 'i') }); + if (await targetOption.count() === 0) { + const availableOptions = await options.allTextContents(); + throw new Error(`Platform "${platformLabel}" not found. Available options: ${availableOptions.join(', ')}`); + } + await targetOption.first().click(); + } + + + /** + * Select LLM model from dropdown + */ + async selectLLMModel(modelLabel: string): Promise { + // Find the LLM Model dropdown (second FormSelect in the LLM Configuration section) + const llmSection = this.page.locator('.form-section').filter({ hasText: /LLM Configuration/i }); + await llmSection.waitFor({ state: 'visible', timeout: 5000 }); + + const modelDropdown = llmSection.locator('.select').nth(1); + await modelDropdown.waitFor({ state: 'visible', timeout: 5000 }); + + // Click the trigger to open dropdown + const trigger = modelDropdown.locator('.select__trigger, .select-trigger, button').first(); + await trigger.waitFor({ state: 'visible', timeout: 5000 }); + await trigger.click(); + + // Scope options to the opened dropdown only + const options = modelDropdown.locator('.select__option, .select-option, .option'); + + const targetOption = options.filter({ hasText: new RegExp(modelLabel, 'i') }); + if (await targetOption.count() === 0) { + const availableOptions = await options.allTextContents(); + throw new Error(`Model "${modelLabel}" not found. Available options: ${availableOptions.join(', ')}`); + } + await targetOption.first().click(); + } + + /** + * Select embedding platform from dropdown + */ + async selectEmbeddingPlatform(platformLabel: string): Promise { + // Find the Embedding Model section + const embeddingSection = this.page.locator('.form-section').filter({ hasText: /Embedding Model Configuration/i }); + await embeddingSection.waitFor({ state: 'visible', timeout: 5000 }); + + const platformDropdown = embeddingSection.locator('.select').first(); + await platformDropdown.waitFor({ state: 'visible', timeout: 5000 }); + + // Click the trigger to open dropdown + const trigger = platformDropdown.locator('.select__trigger, .select-trigger, button').first(); + await trigger.waitFor({ state: 'visible', timeout: 5000 }); + await trigger.click(); + + // Scope options to the opened dropdown only + const options = platformDropdown.locator('.select__option, .select-option, .option'); + + const targetOption = options.filter({ hasText: new RegExp(platformLabel, 'i') }); + if (await targetOption.count() === 0) { + const availableOptions = await options.allTextContents(); + throw new Error(`Embedding platform "${platformLabel}" not found. Available options: ${availableOptions.join(', ')}`); + } + await targetOption.first().click(); + + // Wait for dependent fields to load + await this.page.waitForTimeout(1000); + } + + /** + * Select embedding model from dropdown + */ + async selectEmbeddingModel(modelLabel: string): Promise { + // Find the Embedding Model dropdown (second FormSelect in the embedding section) + const embeddingSection = this.page.locator('.form-section').filter({ hasText: /Embedding Model Configuration/i }); + await embeddingSection.waitFor({ state: 'visible', timeout: 5000 }); + + const modelDropdown = embeddingSection.locator('.select').nth(1); + await modelDropdown.waitFor({ state: 'visible', timeout: 5000 }); + + // Click the trigger to open dropdown + const trigger = modelDropdown.locator('.select__trigger, .select-trigger, button').first(); + await trigger.waitFor({ state: 'visible', timeout: 5000 }); + await trigger.click(); + + // Scope options to the opened dropdown only + const options = modelDropdown.locator('.select__option, .select-option, .option'); + + const targetOption = options.filter({ hasText: new RegExp(modelLabel, 'i') }); + if (await targetOption.count() === 0) { + const availableOptions = await options.allTextContents(); + throw new Error(`Embedding model "${modelLabel}" not found. Available options: ${availableOptions.join(', ')}`); + } + await targetOption.first().click(); + } + + /** + * Fill budget and threshold fields + */ + async fillBudgetFields(monthlyBudget: string, warnBudget: string, stopBudget?: string): Promise { + // Monthly budget + const monthlyBudgetField = this.page.locator('input[name="monthlyBudget"]'); + await monthlyBudgetField.fill(monthlyBudget); + + // Warn budget (percentage - remove % if provided) + const warnBudgetField = this.page.locator('input[name="warnBudget"]'); + const warnValue = warnBudget.replace('%', ''); + await warnBudgetField.fill(warnValue); + + // If stop budget is provided and disconnect checkbox needs to be checked + if (stopBudget) { + // Try multiple approaches to check the checkbox + const disconnectCheckbox = this.page.locator('input[name="disconnectOnBudgetExceed"]'); + + // First, try to find and click the label associated with the checkbox + const checkboxLabel = this.page.locator('label').filter({ + has: disconnectCheckbox + }).or( + this.page.locator('label[for]:has-text("disconnect")').or( + this.page.locator('label:has-text("Disconnect")').or( + this.page.locator('label:has-text("budget exceed")') + ) + ) + ); + + if (await checkboxLabel.count() > 0 && await checkboxLabel.first().isVisible()) { + // Click the label instead of the checkbox + await checkboxLabel.first().click(); + } else { + // Fallback: force check the checkbox even if not visible + await disconnectCheckbox.check({ force: true }); + } + + // Wait for stop budget field to appear + await this.page.waitForTimeout(1000); + + const stopBudgetField = this.page.locator('input[name="stopBudget"]'); + await stopBudgetField.waitFor({ state: 'visible', timeout: 5000 }); + const stopValue = stopBudget.replace('%', ''); + await stopBudgetField.fill(stopValue); + } + } + + /** + * Select deployment environment using radio buttons + */ + async selectDeploymentEnvironment(environment: 'testing' | 'production'): Promise { + const radioOption = this.page.locator(`input[type="radio"][value="${environment}"]`); + await radioOption.check(); + } + + /** + * Fill Azure OpenAI specific credentials + */ + async fillAzureCredentials(deploymentName: string, targetUri: string, apiKey: string): Promise { + // Deployment name + const deploymentField = this.page.locator('input[name="deploymentName"]'); + await deploymentField.waitFor({ state: 'visible' }); + await deploymentField.fill(deploymentName); + + // Target URI + const uriField = this.page.locator('input[name="targetUri"]'); + await uriField.fill(targetUri); + + // API Key + const apiKeyField = this.page.locator('input[name="apiKey"]'); + await apiKeyField.fill(apiKey); + } + + /** + * Fill Azure OpenAI embedding credentials + */ + async fillAzureEmbeddingCredentials(deploymentName: string, targetUri: string, apiKey: string): Promise { + // Embedding deployment name + const embeddingDeploymentField = this.page.locator('input[name="embeddingDeploymentName"]'); + await embeddingDeploymentField.waitFor({ state: 'visible' }); + await embeddingDeploymentField.fill(deploymentName); + + // Embedding target URI + const embeddingUriField = this.page.locator('input[name="embeddingTargetUri"]'); + await embeddingUriField.fill(targetUri); + + // Embedding API Key + const embeddingApiKeyField = this.page.locator('input[name="embeddingAzureApiKey"]'); + await embeddingApiKeyField.fill(apiKey); + } + + /** + * Fill AWS Bedrock specific credentials + */ + async fillAWSCredentials(accessKey: string, secretKey: string): Promise { + // Access key + const accessKeyField = this.page.locator('input[name="accessKey"]'); + await accessKeyField.waitFor({ state: 'visible' }); + await accessKeyField.fill(accessKey); + + // Secret key + const secretKeyField = this.page.locator('input[name="secretKey"]'); + await secretKeyField.fill(secretKey); + } + + /** + * Fill AWS Bedrock embedding credentials + */ + async fillAWSEmbeddingCredentials(accessKey: string, secretKey: string): Promise { + // Embedding access key + const embeddingAccessKeyField = this.page.locator('input[name="embeddingAccessKey"]'); + await embeddingAccessKeyField.waitFor({ state: 'visible' }); + await embeddingAccessKeyField.fill(accessKey); + + // Embedding secret key + const embeddingSecretKeyField = this.page.locator('input[name="embeddingSecretKey"]'); + await embeddingSecretKeyField.fill(secretKey); + } + + /** + * Submit the connection form + */ + async submitConnectionForm(): Promise { + const submitButton = this.page.locator('button[type="submit"]').filter({ hasText: /create connection|update connection/i }); + + // Wait for form to be valid and button to be enabled + await submitButton.waitFor({ state: 'visible' }); + + // Wait for button to be enabled (with timeout) + const maxAttempts = 10; + for (let i = 0; i < maxAttempts; i++) { + if (await submitButton.isEnabled()) { + await submitButton.click(); + await this.page.waitForLoadState('networkidle'); + return; + } + await this.page.waitForTimeout(500); + } + + throw new Error('Submit button remained disabled after filling form'); + } + + /** + * Verify connection creation success + */ + async verifyConnectionSuccess(): Promise { + // Look for success dialog + const successDialog = this.page.locator('[role="dialog"]').filter({ hasText: /connection succeeded|successfully configured/i }); + await successDialog.waitFor({ state: 'visible', timeout: 10000 }); + + // Click the button to go to connections list + const viewConnectionsButton = successDialog.locator('button').filter({ hasText: /view.*connections/i }); + if (await viewConnectionsButton.isVisible()) { + await viewConnectionsButton.click(); + await this.page.waitForLoadState('networkidle'); + } + } + + /** + * Verify connection appears in the list + */ + async verifyConnectionInList(connectionName: string): Promise { + await this.page.waitForTimeout(1000); + const connectionCard = this.page.locator('[class*="connection"]').filter({ hasText: connectionName }); + return (await connectionCard.count()) > 0 && (await connectionCard.first().isVisible()); + } +} + +/** + * Test data factory for LLM connections + */ +export class LLMConnectionTestData { + static createAzureConnection(overrides: Partial<{ + connectionName: string; + llmPlatform: string; + llmModel: string; + embeddingPlatform: string; + embeddingModel: string; + monthlyBudget: string; + warnBudget: string; + stopBudget: string; + deploymentName: string; + targetUri: string; + apiKey: string; + embeddingDeploymentName: string; + embeddingTargetUri: string; + embeddingApiKey: string; + environment: 'testing' | 'production'; + }> = {}) { + const defaultData = { + connectionName: 'Test Azure OpenAI Connection', + llmPlatform: 'Azure', + llmModel: 'GPT-4o', + embeddingPlatform: 'Azure', + embeddingModel: 'text-embedding-3-large', + monthlyBudget: '1000', + warnBudget: '80', + stopBudget: '95', + deploymentName: 'test-gpt4o-deployment', + targetUri: 'https://test-openai.openai.azure.com/', + apiKey: 'sk-test-api-key-azure-12345', + embeddingDeploymentName: 'test-embedding-deployment', + embeddingTargetUri: 'https://test-openai.openai.azure.com/', + embeddingApiKey: 'sk-test-embedding-api-key-azure-67890', + environment: 'testing' as const, + }; + + return { ...defaultData, ...overrides }; + } + + static createAWSConnection(overrides: Partial<{ + connectionName: string; + llmPlatform: string; + llmModel: string; + embeddingPlatform: string; + embeddingModel: string; + monthlyBudget: string; + warnBudget: string; + stopBudget: string; + accessKey: string; + secretKey: string; + embeddingAccessKey: string; + embeddingSecretKey: string; + environment: 'testing' | 'production'; + }> = {}) { + const defaultData = { + connectionName: 'Test AWS Bedrock Connection', + llmPlatform: 'AWS', + llmModel: 'Anthropic Claude 3.5 Sonnet', + embeddingPlatform: 'AWS', + embeddingModel: 'Amazon Titan Text Embeddings V2', + monthlyBudget: '500', + warnBudget: '75', + stopBudget: '90', + accessKey: 'AKIATEST12345', + secretKey: 'test-secret-key-aws-67890', + embeddingAccessKey: 'AKIATEST12345', + embeddingSecretKey: 'test-secret-key-aws-67890', + environment: 'testing' as const, + }; + + return { ...defaultData, ...overrides }; + } +} \ No newline at end of file diff --git a/GUI/tests/helpers/llm-testing-helpers.ts b/GUI/tests/helpers/llm-testing-helpers.ts new file mode 100644 index 00000000..59b5ff4b --- /dev/null +++ b/GUI/tests/helpers/llm-testing-helpers.ts @@ -0,0 +1,277 @@ +import { Page } from '@playwright/test'; + + +/** + * Test LLM Helper - Helper functions for Testing LLM functionality + */ +export class TestLLMHelper { + constructor(private page: Page) {} + + async navigateToTestLLM(): Promise { + await this.page.goto('http://localhost:3003/rag-search/test-llm'); + await this.page.waitForLoadState('networkidle'); + } + + async waitForConnectionsToLoad(): Promise { + // Wait for loading spinner to disappear + const spinner = this.page.locator('.circular-spinner, .spinner, .loading'); + if (await spinner.isVisible()) { + await spinner.waitFor({ state: 'hidden', timeout: 10000 }); + } + + // Wait for the connection dropdown to be visible + await this.page.locator('select[name="connectionId"], .select').first().waitFor({ state: 'visible', timeout: 5000 }); + } + + async selectLLMConnection(connectionLabel: string): Promise { + // Try to find the select element or custom dropdown + const selectElement = this.page.locator('select[name="connectionId"]'); + const customDropdown = this.page.locator('.select').first(); + + if (await selectElement.count() > 0 && await selectElement.isVisible()) { + // Native select element + await selectElement.selectOption({ label: connectionLabel }); + } else if (await customDropdown.count() > 0 && await customDropdown.isVisible()) { + // Custom dropdown (Downshift) + const trigger = customDropdown.locator('.select__trigger'); + await trigger.click(); + + // Wait for options and select + const option = this.page.locator('.select__option').filter({ hasText: new RegExp(connectionLabel, 'i') }); + await option.waitFor({ state: 'visible' }); + await option.click(); + } + } + + async selectFirstAvailableConnection(): Promise { + const customDropdown = this.page.locator('.select').first(); + + if (await customDropdown.isVisible()) { + // Click to open dropdown + const trigger = customDropdown.locator('.select__trigger'); + await trigger.click(); + + // Get first option + const firstOption = this.page.locator('.select__option').first(); + + if (await firstOption.count() > 0) { + const connectionText = await firstOption.textContent(); + await firstOption.click(); + return connectionText; + } + } + + return null; + } + + async fillTestText(text: string): Promise { + const textarea = this.page.locator('textarea').first(); + await textarea.waitFor({ state: 'visible' }); + await textarea.fill(text); + } + + async clearTestText(): Promise { + const textarea = this.page.locator('textarea').first(); + await textarea.waitFor({ state: 'visible' }); + await textarea.clear(); + } + + async clickSendButton(): Promise { + const sendButton = this.page.locator('button').filter({ hasText: /send/i }); + await sendButton.waitFor({ state: 'visible' }); + await sendButton.click(); + } + + async isSendButtonDisabled(): Promise { + const sendButton = this.page.locator('button').filter({ hasText: /send/i }); + return await sendButton.isDisabled(); + } + + async waitForInferenceResult(): Promise { + // Wait for inference result container to appear + await this.page.locator('.inference-results-container, .response-content').waitFor({ + state: 'visible', + timeout: 30000 + }); + } + + async getInferenceResult(): Promise { + const resultContent = this.page.locator('.response-content').first(); + await resultContent.waitFor({ state: 'visible' }); + return await resultContent.textContent() || ''; + } + + async verifyErrorMessage(): Promise { + const errorMessage = this.page.locator('.classification-error, .error-message'); + return await errorMessage.isVisible(); + } + + async verifyLoadingState(): Promise { + const sendButton = this.page.locator('button').filter({ hasText: /sending/i }); + return await sendButton.isVisible(); + } + + async getCharacterCount(): Promise { + const textarea = this.page.locator('textarea').first(); + const value = await textarea.inputValue(); + return value.length; + } + + async verifyMaxLengthIndicator(): Promise { + // Check if max length indicator is visible + const maxLengthIndicator = this.page.locator('.max-length, .character-count'); + return await maxLengthIndicator.isVisible(); + } +} + +/** + * Test Production LLM Helper - Helper functions for Testing Production LLM + */ +export class TestProductionLLMHelper { + constructor(private page: Page) {} + + async navigateToTestProductionLLM(): Promise { + await this.page.goto('http://localhost:3003/rag-search/test-production-llm'); + await this.page.waitForLoadState('networkidle'); + } + + async verifyWelcomeMessage(): Promise { + const welcomeMessage = this.page.locator('.test-production-llm__welcome'); + return await welcomeMessage.isVisible(); + } + + async typeMessage(message: string): Promise { + const textarea = this.page.locator('textarea[name="message"]').or(this.page.locator('textarea[aria-label="Message"]')); + await textarea.waitFor({ state: 'visible' }); + await textarea.fill(message); + } + + async clearMessage(): Promise { + const textarea = this.page.locator('textarea[name="message"]').or(this.page.locator('textarea[aria-label="Message"]')); + await textarea.waitFor({ state: 'visible' }); + await textarea.clear(); + } + + async clickSendButton(): Promise { + const sendButton = this.page.locator('button.test-production-llm__send-button, button').filter({ hasText: /send/i }); + await sendButton.waitFor({ state: 'visible' }); + await sendButton.click(); + } + + async pressEnterToSend(): Promise { + const textarea = this.page.locator('textarea[name="message"]').or(this.page.locator('textarea[aria-label="Message"]')); + await textarea.press('Enter'); + } + + async pressShiftEnterForNewLine(): Promise { + const textarea = this.page.locator('textarea[name="message"]').or(this.page.locator('textarea[aria-label="Message"]')); + await textarea.press('Shift+Enter'); + } + + async isSendButtonDisabled(): Promise { + const sendButton = this.page.locator('button.test-production-llm__send-button, button').filter({ hasText: /send/i }); + return await sendButton.isDisabled(); + } + + async waitForBotResponse(): Promise { + // Wait for typing indicator to appear + const typingIndicator = this.page.locator('.test-production-llm__typing'); + + if (await typingIndicator.isVisible()) { + // Wait for typing indicator to disappear + await typingIndicator.waitFor({ state: 'hidden', timeout: 60000 }); + } + + // Wait for bot message to appear + await this.page.locator('.test-production-llm__message--bot').last().waitFor({ + state: 'visible', + timeout: 60000 + }); + } + + async getUserMessages(): Promise { + const userMessages = this.page.locator('.test-production-llm__message--user .test-production-llm__message-content'); + const count = await userMessages.count(); + const messages: string[] = []; + + for (let i = 0; i < count; i++) { + const text = await userMessages.nth(i).textContent(); + messages.push(text || ''); + } + + return messages; + } + + async getBotMessages(): Promise { + const botMessages = this.page.locator('.test-production-llm__message--bot .test-production-llm__message-content'); + const count = await botMessages.count(); + const messages: string[] = []; + + for (let i = 0; i < count; i++) { + const text = await botMessages.nth(i).textContent(); + messages.push(text || ''); + } + + return messages; + } + + async getMessageCount(): Promise<{ user: number; bot: number }> { + const userCount = await this.page.locator('.test-production-llm__message--user').count(); + const botCount = await this.page.locator('.test-production-llm__message--bot').count(); + + return { user: userCount, bot: botCount }; + } + + async clickClearChat(): Promise { + const clearButton = this.page.locator('button').filter({ hasText: /clear.*chat/i }); + await clearButton.waitFor({ state: 'visible' }); + await clearButton.click(); + } + + async verifyChatCleared(): Promise { + const messages = this.page.locator('.test-production-llm__message'); + const count = await messages.count(); + return count === 0; + } + + async verifyTypingIndicator(): Promise { + const typingIndicator = this.page.locator('.test-production-llm__typing'); + return await typingIndicator.isVisible(); + } + + async verifyMessageTimestamp(messageIndex: number = 0): Promise { + const timestamp = this.page.locator('.test-production-llm__message-timestamp').nth(messageIndex); + return await timestamp.isVisible(); + } + + async scrollToBottom(): Promise { + await this.page.evaluate(() => { + const messagesContainer = document.querySelector('.test-production-llm__messages'); + if (messagesContainer) { + messagesContainer.scrollTop = messagesContainer.scrollHeight; + } + }); + } + + async getLastBotMessage(): Promise { + const lastBotMessage = this.page.locator('.test-production-llm__message--bot').last(); + const content = lastBotMessage.locator('.test-production-llm__message-content'); + return await content.textContent() || ''; + } + + async getLastUserMessage(): Promise { + const lastUserMessage = this.page.locator('.test-production-llm__message--user').last(); + const content = lastUserMessage.locator('.test-production-llm__message-content'); + return await content.textContent() || ''; + } + + async verifyErrorMessage(messageContent: string): Promise { + const botMessages = await this.getBotMessages(); + return botMessages.some(msg => msg.toLowerCase().includes(messageContent.toLowerCase())); + } + + async verifyInputDisabledWhileLoading(): Promise { + const textarea = this.page.locator('textarea[name="message"]').or(this.page.locator('textarea[aria-label="Message"]')); + return await textarea.isDisabled(); + } +} \ No newline at end of file diff --git a/GUI/tests/helpers/test-helpers.ts b/GUI/tests/helpers/test-helpers.ts new file mode 100644 index 00000000..8a4ddd61 --- /dev/null +++ b/GUI/tests/helpers/test-helpers.ts @@ -0,0 +1,533 @@ +import { Page, expect } from '@playwright/test'; + + +/** + * Authentication helper functions for reuse across tests + */ +export class AuthHelper { + constructor(private page: Page) {} + + /** + * Login with Estonian ID + */ + async loginAsAdmin(): Promise { + await this.page.goto('http://localhost:3004/et/dev-auth'); + await this.page.waitForLoadState('networkidle'); + + const idInput = await this.page.locator('input[type="text"], input[placeholder*="user name"], input[placeholder*="sisesta"]').first(); + await idInput.fill('EE30303039914'); + + const submitButton = await this.page.locator('button[type="submit"], button:has-text("sisene"), button:has-text("Sisene"), input[type="submit"]').first(); + await submitButton.click(); + + await this.page.waitForTimeout(20000); + await this.page.waitForLoadState('networkidle', { timeout: 20000 }); + } + + /** + * Verify user is redirected to the correct page based on role + */ + async verifyAdminRedirect(): Promise { + expect(this.page.url()).toContain('user-management'); + } + + /** + * Verify user is redirected to model trainer page + */ + async verifyTrainerRedirect(): Promise { + expect(this.page.url()).toContain('dataset-groups'); + } + + /** + * Click logout button and handle logout process + */ + async logout(): Promise { + const logoutButton = await this.page.locator('button:has-text("Logout")').first(); + + if (await logoutButton.isVisible()) { + await logoutButton.click(); + + // Wait for logout process to complete + await this.page.waitForTimeout(3000); + } else { + throw new Error('Logout button not found'); + } + } + + /** + * Verify user is redirected to login page after logout + */ + async verifyLogoutRedirect(): Promise { + // After logout, user should be redirected to the auth service + await this.page.waitForLoadState('networkidle', { timeout: 10000 }); + + // Check that we're no longer on the application pages + const currentUrl = this.page.url(); + + // Should not contain the main app URLs anymore + expect(currentUrl).not.toContain('/user-management'); + expect(currentUrl).not.toContain('/dataset-groups'); + expect(currentUrl).not.toContain('/llm-connections'); + + // Should redirect to auth service (localhost:3004 for dev) + expect(currentUrl).toContain('localhost:3004'); + } + + /** + * Handle session timeout modal logout + */ + async handleSessionTimeoutLogout(): Promise { + // If session timeout modal appears, click logout there + const sessionModal = await this.page.locator('[role="dialog"]').filter({ hasText: /session.*time.*out/i }); + + if (await sessionModal.isVisible()) { + const modalLogoutButton = await sessionModal.locator('button:has-text("Logout")').first(); + await modalLogoutButton.click(); + await this.page.waitForTimeout(3000); + } + } +} + +/** + * User Management page helper functions + */ +export class UserManagementHelper { + constructor(private page: Page) {} + + /** + * Navigate to user management page + */ + async navigateToUserManagement(): Promise { + await this.page.goto('http://localhost:3003/rag-search/user-management'); + await this.page.waitForLoadState('networkidle'); + } + + /** + * Open the add a user modal + */ + async openAddUserModal(): Promise { + const addUserButton = await this.page.locator('button').filter({ hasText: /add.*user|lisa.*kasutaja/i }).first(); + await addUserButton.click(); + await this.page.waitForTimeout(1000); + + const modal = await this.page.locator('[role="dialog"], .modal, .dialog'); + expect(await modal.isVisible()).toBe(true); + } + + /** + * Close modal by clicking close button or pressing escape + */ + async closeModal(): Promise { + const closeButton = await this.page.locator('button[aria-label*="close"], button:has-text("Cancel"), button:has-text("Close"), .modal-close, [data-testid="close-button"]').first(); + + if (await closeButton.isVisible()) { + await closeButton.click(); + } else { + await this.page.keyboard.press('Escape'); + } + + await this.page.waitForTimeout(1000); + + const modal = await this.page.locator('[role="dialog"], .modal, .dialog'); + expect(await modal.isVisible()).toBe(false); + } + + /** + * Fill user form with test data + */ + async fillUserForm(userData: { + fullName?: string; + idCode?: string; + email?: string; + title?: string; + role?: string; + }): Promise { + if (userData.fullName) { + const nameInput = await this.page.locator('input[placeholder*="name"], input[name*="name"], input[name*="fullName"]').first(); + if (await nameInput.isVisible()) { + await nameInput.fill(userData.fullName); + } + } + + if (userData.idCode) { + const idInput = await this.page.locator('input[placeholder*="id"], input[name*="userid"], input[name*="identification"]').first(); + if (await idInput.isVisible()) { + await idInput.fill(userData.idCode); + } + } + + if (userData.email) { + const emailInput = await this.page.locator('input[type="email"], input[placeholder*="email"], input[name*="email"]').first(); + if (await emailInput.isVisible()) { + await emailInput.fill(userData.email); + } + } + + if (userData.title) { + const titleInput = await this.page.locator('input[placeholder*="title"], input[name*="title"], input[name*="csaTitle"]').first(); + if (await titleInput.isVisible()) { + await titleInput.fill(userData.title); + } + } + + if (userData.role) { + await this.selectRole(userData.role); + } + } + + /** + * Select a role from dropdown + */ + async selectRole(role: string): Promise { + const nativeSelect = await this.page.locator('select[name*="role"], select[name*="authorities"]'); + const reactSelect = await this.page.locator('.react-select__control'); + + if (await nativeSelect.count() > 0 && await nativeSelect.first().isVisible()) { + await nativeSelect.first().selectOption({ label: role }); + } else if (await reactSelect.count() > 0 && await reactSelect.first().isVisible()) { + await reactSelect.first().click(); + await this.page.waitForTimeout(500); + const option = await this.page.locator('.react-select__option').filter({ hasText: role }); + if (await option.isVisible()) { + await option.click(); + } + } + } + + /** + * Submit the user form + */ + async submitForm(): Promise { + // Based on UserModal.tsx, the submit button uses t('global.confirm') = "Confirm" + const submitButton = await this.page.locator('button:has-text("Confirm")').first(); + await submitButton.click(); + await this.page.waitForTimeout(3000); + } + + /** + * Verify success notification appears + */ + async verifySuccessNotification(messagePattern: RegExp = /success|created|added|updated|saved/i): Promise { + const successMessage = await this.page.locator('.toast, .notification, .alert').filter({ hasText: messagePattern }); + if (await successMessage.count() > 0) { + expect(await successMessage.first().isVisible()).toBe(true); + } + } + + /** + * Click edit button for the first user in the table + */ + async editFirstUser(): Promise { + await this.page.waitForSelector('table, [data-testid="data-table"]', { timeout: 10000 }); + + const editButton = await this.page.locator('button[aria-label*="edit"], button:has-text("Edit"), .edit-button, [data-testid*="edit"]').first(); + + if (await editButton.isVisible()) { + await editButton.click(); + await this.page.waitForTimeout(1000); + + const modal = await this.page.locator('[role="dialog"], .modal, .dialog'); + expect(await modal.isVisible()).toBe(true); + return true; + } + + return false; + } + + /** + * Click delete button for the first user and handle confirmation dialog + */ + async deleteFirstUser(): Promise { + await this.page.waitForSelector('table, [data-testid="data-table"]', { timeout: 10000 }); + + const deleteButton = await this.page.locator('button').filter({ hasText: /delete|kustuta/i }).first(); + + if (await deleteButton.isVisible()) { + await deleteButton.click(); + await this.page.waitForTimeout(1000); + + // Verify confirmation dialog opened + const confirmationDialog = await this.page.locator('[role="dialog"], .modal, .dialog'); + expect(await confirmationDialog.isVisible()).toBe(true); + + return true; + } + + return false; + } + + /** + * Confirm deletion in the confirmation dialog + */ + async confirmDeletion(): Promise { + // Based on ActionButtons, the confirm button in delete dialog uses t('global.confirm') = "Confirm" + const confirmButton = await this.page.locator('button:has-text("Confirm")').first(); + await confirmButton.click(); + await this.page.waitForTimeout(2000); + } + + /** + * Cancel deletion in the confirmation dialog + */ + async cancelDeletion(): Promise { + const cancelButton = await this.page.locator('button:has-text("Cancel")').first(); + await cancelButton.click(); + await this.page.waitForTimeout(1000); + + // Verify dialog is closed + const modal = await this.page.locator('[role="dialog"], .modal, .dialog'); + expect(await modal.isVisible()).toBe(false); + } + + /** + * Sort table by column using header click + */ + async sortByColumn(columnName: string): Promise { + const columnHeader = await this.page.locator('th').filter({ hasText: new RegExp(columnName, 'i') }).first(); + if (await columnHeader.isVisible()) { + await columnHeader.click(); + await this.page.waitForTimeout(2000); + } + } + + /** + * Sort table by clicking sort arrows/icons + */ + async sortByArrowIcon(columnName: string, direction: 'asc' | 'desc' = 'asc'): Promise { + // Find the column header + const columnHeader = await this.page.locator('th').filter({ hasText: new RegExp(columnName, 'i') }).first(); + + if (await columnHeader.isVisible()) { + // Look for sort arrow icons within the column header + const sortButton = direction === 'asc' + ? await columnHeader.locator('button, [role="button"]').first() + : await columnHeader.locator('button, [role="button"]').first(); + + if (await sortButton.isVisible()) { + await sortButton.click(); + await this.page.waitForTimeout(2000); + return true; + } + + // Fallback: click the header itself + await columnHeader.click(); + await this.page.waitForTimeout(2000); + return true; + } + + return false; + } + + /** + * Search in a specific column using the search icon + */ + async searchInColumn(columnName: string, searchText: string): Promise { + // Find the column header + const columnHeader = await this.page.locator('th').filter({ hasText: new RegExp(columnName, 'i') }).first(); + + if (await columnHeader.isVisible()) { + // Look for search icon button + const searchButton = await columnHeader.locator('button').first(); + + if (await searchButton.isVisible()) { + await searchButton.click(); + await this.page.waitForTimeout(500); + + // Look for the search input that appears + const searchInput = await this.page.locator('.data-table__filter input[type="text"]').first(); + + if (await searchInput.isVisible()) { + await searchInput.fill(searchText); + await this.page.waitForTimeout(2000); + return true; + } + } + } + + return false; + } + + /** + * Clear search in a column + */ + async clearColumnSearch(columnName: string): Promise { + // Find the column header + const columnHeader = await this.page.locator('th').filter({ hasText: new RegExp(columnName, 'i') }).first(); + + if (await columnHeader.isVisible()) { + // Look for search icon button + const searchButton = await columnHeader.locator('button').first(); + + if (await searchButton.isVisible()) { + await searchButton.click(); + await this.page.waitForTimeout(500); + + // Clear the search input + const searchInput = await this.page.locator('.data-table__filter input[type="text"]').first(); + + if (await searchInput.isVisible()) { + await searchInput.clear(); + await this.page.waitForTimeout(1000); + + // Click outside to close search + await this.page.keyboard.press('Escape'); + await this.page.waitForTimeout(500); + } + } + } + } + + /** + * Verify table is sorted correctly + */ + async verifyTableSort(columnIndex: number, expectedOrder: 'asc' | 'desc' = 'asc'): Promise { + await this.page.waitForTimeout(1000); + + // Get all cell values from the specified column + const cells = await this.page.locator(`table tbody tr td:nth-child(${columnIndex + 1}), [data-testid="data-table"] tbody tr td:nth-child(${columnIndex + 1})`); + + const cellTexts = []; + const count = await cells.count(); + + for (let i = 0; i < Math.min(count, 5); i++) { // Check first 5 rows + const text = await cells.nth(i).textContent(); + if (text?.trim()) { + cellTexts.push(text.trim()); + } + } + + if (cellTexts.length < 2) return true; // Can't verify sort with less than 2 items + + // Check if sorted correctly + const sorted = [...cellTexts].sort((a, b) => { + if (expectedOrder === 'asc') { + return a.localeCompare(b, undefined, { numeric: true }); + } else { + return b.localeCompare(a, undefined, { numeric: true }); + } + }); + + return JSON.stringify(cellTexts) === JSON.stringify(sorted); + } + + /** + * Verify search results contain search text + */ + async verifySearchResults(columnIndex: number, searchText: string): Promise { + await this.page.waitForTimeout(1000); + + // Get visible table rows + const rows = await this.page.locator('table tbody tr, [data-testid="data-table"] tbody tr'); + const rowCount = await rows.count(); + + if (rowCount === 0) return true; // Empty results are valid for no matches + + // Check that all visible rows contain the search text in the specified column + for (let i = 0; i < Math.min(rowCount, 5); i++) { + const cell = await rows.nth(i).locator(`td:nth-child(${columnIndex + 1})`); + const cellText = await cell.textContent(); + + if (cellText && !cellText.toLowerCase().includes(searchText.toLowerCase())) { + return false; + } + } + + return true; + } + + /** + * Navigate to next page if pagination is available + */ + async goToNextPage(): Promise { + const nextButton = await this.page.locator('button[aria-label*="next"], button:has-text("Next"), .pagination-next'); + + if (await nextButton.count() > 0 && await nextButton.first().isEnabled()) { + await nextButton.first().click(); + await this.page.waitForTimeout(2000); + return true; + } + + return false; + } + + /** + * Navigate to previous page if pagination is available + */ + async goToPreviousPage(): Promise { + const prevButton = await this.page.locator('button[aria-label*="previous"], button:has-text("Previous"), .pagination-prev'); + + if (await prevButton.count() > 0 && await prevButton.first().isEnabled()) { + await prevButton.first().click(); + await this.page.waitForTimeout(2000); + return true; + } + + return false; + } +} + +/** + * Common page utilities + */ +export class PageHelper { + constructor(private page: Page) {} + + /** + * Take a screenshot with automatic path generation + */ + async takeScreenshot(name: string): Promise { + await this.page.screenshot({ path: `tests/screenshots/${name}.png` }); + } + + /** + * Wait for table to load + */ + async waitForTable(): Promise { + await this.page.waitForSelector('table, [data-testid="data-table"]', { timeout: 10000 }); + } + + /** + * Check if element exists and is visible + */ + async isElementVisible(selector: string): Promise { + const element = await this.page.locator(selector); + return await element.count() > 0 && await element.first().isVisible(); + } +} + +/** + * Test data factory for creating test users + */ +export class TestDataFactory { + static createTestUser(overrides: Partial<{ + fullName: string; + idCode: string; + email: string; + title: string; + role: string; + }> = {}) { + const defaultUser = { + fullName: 'Test User Name', + idCode: 'EE12345678901', + email: 'test.user@example.com', + title: 'Test Manager', + role: 'MODEL_TRAINER' + }; + + return { ...defaultUser, ...overrides }; + } + + static createAdminUser(overrides: Partial<{ + fullName: string; + idCode: string; + email: string; + title: string; + role: string; + }> = {}) { + return this.createTestUser({ + role: 'ROLE_ADMINISTRATOR', + title: 'System Administrator', + ...overrides + }); + } +} diff --git a/GUI/tests/llm-connections.spec.ts b/GUI/tests/llm-connections.spec.ts new file mode 100644 index 00000000..193725b9 --- /dev/null +++ b/GUI/tests/llm-connections.spec.ts @@ -0,0 +1,913 @@ +import { test, expect } from '@playwright/test'; +import { AuthHelper, PageHelper } from './helpers/test-helpers'; +import { LLMConnectionsHelper, LLMConnectionTestData } from './helpers/llm-connections-helpers'; + + +test.describe('LLM Connections', () => { + let authHelper: AuthHelper; + let pageHelper: PageHelper; + let llmConnectionsHelper: LLMConnectionsHelper; + + // Setup: Login as admin before each test + test.beforeEach(async ({ page }) => { + authHelper = new AuthHelper(page); + pageHelper = new PageHelper(page); + llmConnectionsHelper = new LLMConnectionsHelper(page); + + await test.step('Login as administrator', async () => { + await authHelper.loginAsAdmin(); + await authHelper.verifyAdminRedirect(); + }); + + await test.step('Navigate to LLM connections page', async () => { + await page.goto('http://localhost:3001/rag-search/llm-connections'); + await page.waitForLoadState('networkidle'); + }); + }); + + test('should view LLM connections list page', async ({ page }) => { + await test.step('Verify LLM connections page loads', async () => { + // Check page title or heading + const pageTitle = await page.locator('h1, h2, h3, .title').filter({ hasText: /data.*models/i }).first(); + expect(await pageTitle.isVisible()).toBe(true); + + // Verify create connection button is present + const createButton = await page.locator('button').filter({ hasText: /create.*connection|add.*connection|new.*connection/i }); + expect(await createButton.isVisible()).toBe(true); + + await pageHelper.takeScreenshot('llm-connections-list'); + }); + + await test.step('Verify connections list structure', async () => { + // Check for connections grid/list container + const connectionsContainer = await page.locator('.connections-grid, .connections-list, .llm-connections-container'); + + if (await connectionsContainer.count() > 0) { + expect(await connectionsContainer.first().isVisible()).toBe(true); + } + + // Check for filter/sort controls + const filterControls = await page.locator('select, .filter, .sort'); + if (await filterControls.count() > 0) { + expect(await filterControls.first().isVisible()).toBe(true); + } + + await pageHelper.takeScreenshot('connections-structure'); + }); + + await test.step('Verify pagination if present', async () => { + const paginationContainer = await page.locator('.pagination, nav[aria-label*="pagination"]'); + + if (await paginationContainer.count() > 0) { + expect(await paginationContainer.first().isVisible()).toBe(true); + await pageHelper.takeScreenshot('connections-pagination'); + } + }); + }); + + test('should inspect available platform options (debugging)', async ({ page }) => { + await test.step('Navigate to create connection page', async () => { + await llmConnectionsHelper.navigateToCreateConnection(); + expect(page.url()).toContain('create-llm-connection'); + await pageHelper.takeScreenshot('debug-form-loaded'); + }); + + await test.step('Check available platform options', async () => { + // Find the LLM Configuration section and platform dropdown + const llmSection = page.locator('.form-section').filter({ hasText: /LLM Configuration/i }); + await llmSection.waitFor({ state: 'visible' }); + + const platformDropdown = llmSection.locator('.select').first(); + await platformDropdown.waitFor({ state: 'visible' }); + + // Click the trigger to open dropdown + const trigger = platformDropdown.locator('.select__trigger'); + await trigger.click(); + + // Wait for options to appear + const options = page.locator('.select__option'); + await options.first().waitFor({ state: 'visible', timeout: 10000 }); + + // Log available platform options + const availableOptions = await options.allTextContents(); + console.log('Available platform options:', availableOptions); + + await pageHelper.takeScreenshot('debug-platform-options'); + + // Close dropdown + await page.keyboard.press('Escape'); + }); + }); + + test('should create new Azure OpenAI LLM connection', async ({ page }) => { + const azureData = LLMConnectionTestData.createAzureConnection({ + connectionName: 'Test Azure Connection ' + Date.now(), + environment: 'testing' + }); + + await test.step('Navigate to create connection page', async () => { + await llmConnectionsHelper.navigateToCreateConnection(); + + // Verify we're on the create page + expect(page.url()).toContain('create-llm-connection'); + await pageHelper.takeScreenshot('azure-create-form-loaded'); + }); + + await test.step('Fill connection basic information', async () => { + // Connection name + await llmConnectionsHelper.fillConnectionName(azureData.connectionName); + + // Platform selection + await llmConnectionsHelper.selectLLMPlatform(azureData.llmPlatform); + + // Model selection (wait for platform to load models) + await llmConnectionsHelper.selectLLMModel(azureData.llmModel); + + await pageHelper.takeScreenshot('azure-basic-info-filled'); + }); + + await test.step('Fill Azure OpenAI credentials', async () => { + // Fill Azure-specific LLM credentials + await llmConnectionsHelper.fillAzureCredentials( + azureData.deploymentName, + azureData.targetUri, + azureData.apiKey + ); + + await pageHelper.takeScreenshot('azure-llm-credentials-filled'); + }); + + await test.step('Configure embedding model', async () => { + // Embedding platform + await llmConnectionsHelper.selectEmbeddingPlatform(azureData.embeddingPlatform); + + // Embedding model + await llmConnectionsHelper.selectEmbeddingModel(azureData.embeddingModel); + + // Fill Azure embedding credentials + await llmConnectionsHelper.fillAzureEmbeddingCredentials( + azureData.embeddingDeploymentName, + azureData.embeddingTargetUri, + azureData.embeddingApiKey + ); + + await pageHelper.takeScreenshot('azure-embedding-configured'); + }); + + await test.step('Configure budget and deployment', async () => { + // Budget fields + await llmConnectionsHelper.fillBudgetFields( + azureData.monthlyBudget, + azureData.warnBudget, + azureData.stopBudget + ); + + // Deployment environment + await llmConnectionsHelper.selectDeploymentEnvironment(azureData.environment); + + await pageHelper.takeScreenshot('azure-budget-deployment-configured'); + }); + + await test.step('Submit and verify Azure connection', async () => { + // Submit the form + await llmConnectionsHelper.submitConnectionForm(); + + // Verify success + await llmConnectionsHelper.verifyConnectionSuccess(); + + await pageHelper.takeScreenshot('azure-connection-success'); + }); + }); + + test('should create new AWS Bedrock LLM connection', async ({ page }) => { + const awsData = LLMConnectionTestData.createAWSConnection({ + connectionName: 'Test AWS Connection ' + Date.now(), + environment: 'testing' + }); + + await test.step('Navigate to create connection page', async () => { + await llmConnectionsHelper.navigateToCreateConnection(); + + // Verify we're on the create page + expect(page.url()).toContain('create-llm-connection'); + await pageHelper.takeScreenshot('aws-create-form-loaded'); + }); + + await test.step('Fill connection basic information', async () => { + // Connection name + await llmConnectionsHelper.fillConnectionName(awsData.connectionName); + + // Platform selection + await llmConnectionsHelper.selectLLMPlatform(awsData.llmPlatform); + + // Model selection (wait for platform to load models) + await llmConnectionsHelper.selectLLMModel(awsData.llmModel); + + await pageHelper.takeScreenshot('aws-basic-info-filled'); + }); + + await test.step('Fill AWS Bedrock credentials', async () => { + // Fill AWS-specific LLM credentials + await llmConnectionsHelper.fillAWSCredentials( + awsData.accessKey, + awsData.secretKey + ); + + await pageHelper.takeScreenshot('aws-llm-credentials-filled'); + }); + + await test.step('Configure embedding model', async () => { + // Embedding platform + await llmConnectionsHelper.selectEmbeddingPlatform(awsData.embeddingPlatform); + + // Embedding model + await llmConnectionsHelper.selectEmbeddingModel(awsData.embeddingModel); + + // Fill AWS embedding credentials + await llmConnectionsHelper.fillAWSEmbeddingCredentials( + awsData.embeddingAccessKey, + awsData.embeddingSecretKey + ); + + await pageHelper.takeScreenshot('aws-embedding-configured'); + }); + + await test.step('Configure budget and deployment', async () => { + // Budget fields + await llmConnectionsHelper.fillBudgetFields( + awsData.monthlyBudget, + awsData.warnBudget, + awsData.stopBudget + ); + + // Deployment environment + await llmConnectionsHelper.selectDeploymentEnvironment(awsData.environment); + + await pageHelper.takeScreenshot('aws-budget-deployment-configured'); + }); + + await test.step('Submit and verify AWS connection', async () => { + // Submit the form + await llmConnectionsHelper.submitConnectionForm(); + + // Verify success + await llmConnectionsHelper.verifyConnectionSuccess(); + + await pageHelper.takeScreenshot('aws-connection-success'); + }); + + await test.step('Verify connection appears in list', async () => { + // Verify the connection exists in the list + const exists = await llmConnectionsHelper.verifyConnectionInList(awsData.connectionName); + expect(exists).toBe(true); + + await pageHelper.takeScreenshot('aws-connection-in-list'); + }); + }); + + test('should view connection details', async ({ page }) => { + await test.step('Navigate to connections list', async () => { + await llmConnectionsHelper.navigateToLLMConnections(); + await pageHelper.takeScreenshot('view-connections-list'); + }); + + await test.step('Click on a connection to view details', async () => { + // Find any connection card + const connectionCard = page.locator('[class*="connection-card"], [class*="llm-connection"]').first(); + + if (await connectionCard.count() > 0) { + // Look for view/details button or click the card + const viewButton = connectionCard.locator('button').filter({ hasText: /Settings|open/i }); + + if (await viewButton.count() > 0 && await viewButton.isVisible()) { + await viewButton.click(); + } else { + // Click the card itself + await connectionCard.click(); + } + + await page.waitForLoadState('networkidle'); + + // Verify we're on the view page + expect(page.url()).toMatch(/view-llm-connection|llm-connection\/\d+/); + + await pageHelper.takeScreenshot('connection-details-view'); + } + }); + + await test.step('Verify connection details are displayed', async () => { + // Check for connection details sections + const detailsContainer = page.locator('.connection-details, .details-container, .view-container'); + + if (await detailsContainer.count() > 0) { + expect(await detailsContainer.isVisible()).toBe(true); + } + + // Check for key information fields + const expectedFields = [ + /connection.*name/i, + /platform/i, + /model/i, + /environment/i + ]; + + for (const fieldPattern of expectedFields) { + const field = page.locator('label, .field-label, .detail-label').filter({ hasText: fieldPattern }); + + if (await field.count() > 0) { + console.log(`Found field: ${fieldPattern}`); + } + } + + await pageHelper.takeScreenshot('connection-details-displayed'); + }); + }); + + test('should update/edit LLM connection', async ({ page }) => { + await test.step('Navigate to connections list', async () => { + await llmConnectionsHelper.navigateToLLMConnections(); + }); + + await test.step('Open edit form via Settings button', async () => { + // Find first connection cad + const connectionCard = page.locator('.dataset-group-card').first(); + + if (await connectionCard.count() > 0) { + // Look for Settings button in the button-row + const settingsButton = connectionCard.locator('.button-row button').filter({ hasText: /settings/i }); + + if (await settingsButton.count() > 0 && await settingsButton.isVisible()) { + await settingsButton.click(); + await page.waitForLoadState('networkidle'); + + // Verify we're on the view/edit page with query parameter + expect(page.url()).toMatch(/view-llm-connection\?id=/); + + await pageHelper.takeScreenshot('connection-settings-page'); + } else { + console.log('Settings button not found, skipping edit test'); + return; + } + } else { + console.log('No connection cards found, skipping edit test'); + return; + } + }); + + await test.step('Verify Update Connection button is initially disabled', async () => { + const updateButton = page.locator('button[type="submit"]').filter({ + hasText: /update.*connection/i + }); + + if (await updateButton.count() > 0 && await updateButton.isVisible()) { + const isDisabled = await updateButton.isDisabled(); + expect(isDisabled).toBe(true); + + await pageHelper.takeScreenshot('update-button-initially-disabled'); + } + }); + + await test.step('Update connection name to enable submit button', async () => { + const nameField = page.locator('input[name="connectionName"]'); + + if (await nameField.count() > 0 && await nameField.isVisible()) { + // Get current value + const currentValue = await nameField.inputValue(); + + // Modify the connection name + const newValue = `${currentValue} - Updated ${Date.now()}`; + + await nameField.clear(); + await nameField.fill(newValue); + + // Wait for form validation + await page.waitForTimeout(500); + + await pageHelper.takeScreenshot('connection-name-updated'); + } + }); + + await test.step('Update monthly budget', async () => { + const budgetField = page.locator('input[name="monthlyBudget"]'); + + if (await budgetField.count() > 0 && await budgetField.isVisible()) { + await budgetField.clear(); + await budgetField.fill('2000'); + + // Wait for validation + await page.waitForTimeout(500); + + await pageHelper.takeScreenshot('budget-updated'); + } + }); + + await test.step('Verify Update Connection button is now enabled', async () => { + const updateButton = page.locator('button[type="submit"]').filter({ + hasText: /update.*connection/i + }); + + if (await updateButton.count() > 0 && await updateButton.isVisible()) { + // Wait for button to be enabled after field changes + await page.waitForTimeout(1000); + + const isEnabled = await updateButton.isEnabled(); + expect(isEnabled).toBe(true); + + await pageHelper.takeScreenshot('update-button-enabled'); + } + }); + + await test.step('Submit update', async () => { + const updateButton = page.locator('button[type="submit"]').filter({ + hasText: /update.*connection/i + }); + + if (await updateButton.count() > 0 && await updateButton.isEnabled()) { + await updateButton.click(); + await page.waitForLoadState('networkidle'); + + // Look for success message + const successMessage = page.locator('[role="dialog"], .success, .notification, .toast').filter({ + hasText: /success|updated|saved/i + }); + + if (await successMessage.count() > 0) { + await successMessage.waitFor({ state: 'visible', timeout: 5000 }); + expect(await successMessage.isVisible()).toBe(true); + await pageHelper.takeScreenshot('update-success'); + } + } + }); + + await test.step('Verify Delete button exists at bottom', async () => { + // Scroll to bottom of page to find delete btn + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + await page.waitForTimeout(500); + + const deleteButton = page.locator('button').filter({ hasText: /delete/i }); + + if (await deleteButton.count() > 0) { + expect(await deleteButton.isVisible()).toBe(true); + console.log('Delete button found at bottom of page'); + + await pageHelper.takeScreenshot('delete-button-at-bottom'); + } + }); + }); + + test('should filter connections by platform', async ({ page }) => { + await test.step('Navigate to connections list', async () => { + await llmConnectionsHelper.navigateToLLMConnections(); + }); + + await test.step('Locate and use platform filter', async () => { + // Look for platform filter dropdown + const platformFilter = page.locator('select, .filter, .select').filter({ + hasText: /platform|filter.*platform/i + }).or( + page.locator('label').filter({ hasText: /platform/i }).locator('..').locator('select, .select') + ).first(); + + if (await platformFilter.count() > 0) { + await platformFilter.waitFor({ state: 'visible', timeout: 5000 }); + + // Check if it's a select element or custom dropdown + const isNativeSelect = await platformFilter.evaluate(el => el.tagName === 'SELECT'); + + if (isNativeSelect) { + // Native select + await platformFilter.selectOption({ index: 1 }); // Select first non-default option + } else { + // Custom dropdown + const trigger = platformFilter.locator('.select__trigger, button').first(); + await trigger.click(); + + // Select first option + const option = page.locator('.select__option').nth(1); + await option.waitFor({ state: 'visible' }); + await option.click(); + } + + await page.waitForTimeout(1000); + await pageHelper.takeScreenshot('platform-filter-applied'); + + // Verify filter was applied (URL params or filtered results) + const url = page.url(); + console.log('URL after filter:', url); + } else { + console.log('Platform filter not found'); + } + }); + }); + + test('should filter connections by LLM model', async ({ page }) => { + await test.step('Navigate to connections list', async () => { + await llmConnectionsHelper.navigateToLLMConnections(); + }); + + await test.step('Locate and use model filter', async () => { + // Look for model filter dropdown + const modelFilter = page.locator('select, .filter, .select').filter({ + hasText: /model|filter.*model/i + }).or( + page.locator('label').filter({ hasText: /model/i }).locator('..').locator('select, .select') + ).first(); + + if (await modelFilter.count() > 0) { + await modelFilter.waitFor({ state: 'visible', timeout: 5000 }); + + const isNativeSelect = await modelFilter.evaluate(el => el.tagName === 'SELECT'); + + if (isNativeSelect) { + await modelFilter.selectOption({ index: 1 }); + } else { + const trigger = modelFilter.locator('.select__trigger, button').first(); + await trigger.click(); + + const option = page.locator('.select__option').nth(1); + await option.waitFor({ state: 'visible' }); + await option.click(); + } + + await page.waitForTimeout(1000); + await pageHelper.takeScreenshot('model-filter-applied'); + } else { + console.log('Model filter not found'); + } + }); + }); + + test('should filter connections by environment', async ({ page }) => { + await test.step('Navigate to connections list', async () => { + await llmConnectionsHelper.navigateToLLMConnections(); + }); + + await test.step('Locate and use environment filter', async () => { + // Look for environment filter + const environmentFilter = page.locator('select, .filter, .select').filter({ + hasText: /environment|deployment/i + }).or( + page.locator('label').filter({ hasText: /environment/i }).locator('..').locator('select, .select') + ).first(); + + if (await environmentFilter.count() > 0) { + await environmentFilter.waitFor({ state: 'visible', timeout: 5000 }); + + const isNativeSelect = await environmentFilter.evaluate(el => el.tagName === 'SELECT'); + + if (isNativeSelect) { + // Try to select 'testing' or 'production' + await environmentFilter.selectOption('testing').catch(() => + environmentFilter.selectOption({ index: 1 }) + ); + } else { + const trigger = environmentFilter.locator('.select__trigger, button').first(); + await trigger.click(); + + const testingOption = page.locator('.select__option').filter({ hasText: /testing/i }); + + if (await testingOption.count() > 0) { + await testingOption.first().click(); + } else { + await page.locator('.select__option').nth(1).click(); + } + } + + await page.waitForTimeout(1000); + await pageHelper.takeScreenshot('environment-filter-applied'); + } else { + console.log('Environment filter not found'); + } + }); + }); + + test('should sort connections by created date', async ({ page }) => { + await test.step('Navigate to connections list', async () => { + await llmConnectionsHelper.navigateToLLMConnections(); + }); + + await test.step('Locate and use sort control', async () => { + // Look for sort dropdown or button + const sortControl = page.locator('select, .sort, .select').filter({ + hasText: /sort|order/i + }).or( + page.locator('button').filter({ hasText: /sort/i }) + ).first(); + + if (await sortControl.count() > 0) { + await sortControl.waitFor({ state: 'visible', timeout: 5000 }); + + const isButton = await sortControl.evaluate(el => el.tagName === 'BUTTON'); + + if (isButton) { + // Click sort button + await sortControl.click(); + await pageHelper.takeScreenshot('sort-clicked'); + } else { + // Select sort option + const isNativeSelect = await sortControl.evaluate(el => el.tagName === 'SELECT'); + + if (isNativeSelect) { + await sortControl.selectOption({ index: 1 }); + } else { + const trigger = sortControl.locator('.select__trigger, button').first(); + await trigger.click(); + + const option = page.locator('.select__option').nth(1); + await option.waitFor({ state: 'visible' }); + await option.click(); + } + } + + await page.waitForTimeout(1000); + await pageHelper.takeScreenshot('sort-applied'); + } else { + console.log('Sort control not found'); + } + }); + + await test.step('Verify sort order changed', async () => { + // Get connection cards + const connectionCards = page.locator('[class*="connection-card"], [class*="llm-connection"]'); + const count = await connectionCards.count(); + + console.log(`Found ${count} connections after sort`); + + if (count > 0) { + await pageHelper.takeScreenshot('connections-after-sort'); + } + }); + }); + + test('should reset filters', async ({ page }) => { + await test.step('Navigate to connections list', async () => { + await llmConnectionsHelper.navigateToLLMConnections(); + }); + + await test.step('Apply a filter', async () => { + const platformFilter = page.locator('select, .filter, .select').first(); + + if (await platformFilter.count() > 0 && await platformFilter.isVisible()) { + const isNativeSelect = await platformFilter.evaluate(el => el.tagName === 'SELECT'); + + if (isNativeSelect) { + await platformFilter.selectOption({ index: 1 }); + } else { + const trigger = platformFilter.locator('.select__trigger, button').first(); + await trigger.click(); + + await page.locator('.select__option').nth(1).click(); + } + + await page.waitForTimeout(500); + } + }); + + await test.step('Click reset button', async () => { + const resetButton = page.locator('button').filter({ hasText: /reset|clear.*filter/i }); + + if (await resetButton.count() > 0 && await resetButton.isVisible()) { + await resetButton.click(); + await page.waitForTimeout(1000); + + await pageHelper.takeScreenshot('filters-reset'); + + // Verify filters were reset (check URL or filter values) + console.log('Filters reset, URL:', page.url()); + } else { + console.log('Reset button not found'); + } + }); + }); + + test('should navigate through pagination', async ({ page }) => { + await test.step('Navigate to connections list', async () => { + await llmConnectionsHelper.navigateToLLMConnections(); + }); + + await test.step('Check if pagination exists', async () => { + const pagination = page.locator('.pagination, nav[aria-label*="pagination"], [class*="pagination"]'); + + if (await pagination.count() > 0) { + expect(await pagination.isVisible()).toBe(true); + await pageHelper.takeScreenshot('pagination-visible'); + + // Look for next button + const nextButton = pagination.locator('button').filter({ + hasText: /next|>/i + }).or( + pagination.locator('button[aria-label*="next"]') + ).first(); + + if (await nextButton.count() > 0 && await nextButton.isEnabled()) { + // Get current page connections + const beforeCards = await page.locator('[class*="connection-card"], [class*="llm-connection"]').count(); + + // Click next + await nextButton.click(); + await page.waitForLoadState('networkidle'); + await page.waitForTimeout(1000); + + await pageHelper.takeScreenshot('pagination-next-page'); + + // Verify page changed (URL param or different content) + const afterCards = await page.locator('[class*="connection-card"], [class*="llm-connection"]').count(); + console.log(`Before: ${beforeCards} cards, After: ${afterCards} cards`); + } + } else { + console.log('Pagination not found - likely fewer connections than page size'); + } + }); + + await test.step('Navigate back to first page', async () => { + const pagination = page.locator('.pagination, nav[aria-label*="pagination"], [class*="pagination"]'); + + if (await pagination.count() > 0) { + const previousButton = pagination.locator('button').filter({ + hasText: /previous|prev| 0 && await previousButton.isEnabled()) { + await previousButton.click(); + await page.waitForLoadState('networkidle'); + await page.waitForTimeout(1000); + + await pageHelper.takeScreenshot('pagination-previous-page'); + } + } + }); + }); + + test('should delete LLM connection', async ({ page }) => { + await test.step('Navigate to connections list', async () => { + await llmConnectionsHelper.navigateToLLMConnections(); + }); + + await test.step('Open connection settings via Settings button', async () => { + // Look for a connection card + const connectionCards = page.locator('.dataset-group-card'); + + if (await connectionCards.count() > 0) { + const firstCard = connectionCards.first(); + + // Look for Settings button in the button-row + const settingsButton = firstCard.locator('.button-row button').filter({ hasText: /settings/i }); + + if (await settingsButton.count() > 0 && await settingsButton.isVisible()) { + await settingsButton.click(); + await page.waitForLoadState('networkidle'); + + // Verify we're on the view/edit page with query parameter + expect(page.url()).toMatch(/view-llm-connection\?id=/); + + await pageHelper.takeScreenshot('connection-settings-page-for-delete'); + } else { + console.log('Settings button not found, skipping delete test'); + return; + } + } else { + console.log('No connection cards found, skipping delete test'); + return; + } + }); + + await test.step('Scroll to bottom and click delete button', async () => { + // Scroll to bottom of page to find delete button + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + await page.waitForTimeout(500); + + const deleteButton = page.locator('button').filter({ hasText: /delete/i }); + + if (await deleteButton.count() > 0 && await deleteButton.isVisible()) { + await deleteButton.click(); + await pageHelper.takeScreenshot('delete-button-clicked'); + } else { + console.log('Delete button not found at bottom of page'); + return; + } + }); + + await test.step('Confirm deletion in modal', async () => { + // Look for confirmation dialog + const confirmDialog = page.locator('[role="dialog"], .modal, .dialog').filter({ + hasText: /delete|confirm|remove/i + }); + + if (await confirmDialog.count() > 0) { + expect(await confirmDialog.isVisible()).toBe(true); + + await pageHelper.takeScreenshot('delete-confirmation-dialog'); + + // Look for confirm/delete button in dialog + const confirmButton = confirmDialog.locator('button').filter({ + hasText: /delete|confirm|yes|remove/i + }); + + if (await confirmButton.count() > 0) { + await confirmButton.click(); + await page.waitForLoadState('networkidle'); + await page.waitForTimeout(1000); + + // Look for success message + const successMessage = page.locator('[role="alert"], .notification, .toast').filter({ + hasText: /success|deleted|removed/i + }); + + if (await successMessage.count() > 0) { + expect(await successMessage.isVisible()).toBe(true); + await pageHelper.takeScreenshot('delete-success'); + } + } + } else { + console.log('Delete confirmation dialog not found'); + } + }); + }); + + test('should display no data message when no connections exist', async ({ page }) => { + await test.step('Navigate to connections list', async () => { + await llmConnectionsHelper.navigateToLLMConnections(); + }); + + await test.step('Check for connections or no data message', async () => { + // Look for connections + const connectionCards = page.locator('[class*="connection-card"], [class*="llm-connection"]'); + const hasConnections = await connectionCards.count() > 0; + + if (!hasConnections) { + // Look for no data message + const noDataMessage = page.locator('.no-data, .empty-state, [class*="no-data"]').or( + page.locator('p, div').filter({ hasText: /no.*connection|no.*model|empty/i }) + ); + + if (await noDataMessage.count() > 0) { + expect(await noDataMessage.isVisible()).toBe(true); + await pageHelper.takeScreenshot('no-data-message'); + } + } else { + console.log(`Found ${await connectionCards.count()} connections`); + await pageHelper.takeScreenshot('connections-exist'); + } + }); + }); + + test('should display production and testing connections separately', async ({ page }) => { + await test.step('Navigate to connections list', async () => { + await llmConnectionsHelper.navigateToLLMConnections(); + }); + + await test.step('Check for production connections section', async () => { + const productionSection = page.locator('h2, h3, .section-title').filter({ + hasText: /production/i + }); + + if (await productionSection.count() > 0) { + expect(await productionSection.isVisible()).toBe(true); + await pageHelper.takeScreenshot('production-section'); + } + }); + + await test.step('Check for testing/other connections section', async () => { + const testingSection = page.locator('h2, h3, .section-title, p').filter({ + hasText: /testing|other.*connection/i + }); + + if (await testingSection.count() > 0) { + expect(await testingSection.isVisible()).toBe(true); + await pageHelper.takeScreenshot('testing-section'); + } + }); + }); + + test('should display connection status (active/inactive)', async ({ page }) => { + await test.step('Navigate to connections list', async () => { + await llmConnectionsHelper.navigateToLLMConnections(); + }); + + await test.step('Check connection status badges', async () => { + const connectionCards = page.locator('[class*="connection-card"], [class*="llm-connection"]'); + + if (await connectionCards.count() > 0) { + const firstCard = connectionCards.first(); + + // Look for status indicator + const statusBadge = firstCard.locator('.status, .badge, [class*="status"]').or( + firstCard.locator('span').filter({ hasText: /active|inactive/i }) + ); + + if (await statusBadge.count() > 0) { + expect(await statusBadge.isVisible()).toBe(true); + + const statusText = await statusBadge.textContent(); + console.log('Connection status:', statusText); + + await pageHelper.takeScreenshot('connection-status-displayed'); + } + } + }); + }); + + +}); diff --git a/GUI/tests/llm-testing.spec.ts b/GUI/tests/llm-testing.spec.ts new file mode 100644 index 00000000..bedaac54 --- /dev/null +++ b/GUI/tests/llm-testing.spec.ts @@ -0,0 +1,253 @@ +import { test, expect} from '@playwright/test'; +import { AuthHelper, PageHelper } from './helpers/test-helpers'; +import { TestLLMHelper,TestProductionLLMHelper } from './helpers/llm-testing-helpers'; + + +// Test LLM Page + +test.describe('Test LLM Page', () => { + let authHelper: AuthHelper; + let pageHelper: PageHelper; + let testLLMHelper: TestLLMHelper; + + test.beforeEach(async ({ page }) => { + authHelper = new AuthHelper(page); + pageHelper = new PageHelper(page); + testLLMHelper = new TestLLMHelper(page); + + await test.step('Login as administrator', async () => { + await authHelper.loginAsAdmin(); + await authHelper.verifyAdminRedirect(); + }); + + await test.step('Navigate to Test LLM page', async () => { + await testLLMHelper.navigateToTestLLM(); + }); + }); + + test('should load Test LLM page successfully', async ({ page }) => { + await test.step('Verify page title and components', async () => { + // Check page title + const pageTitle = page.locator('.title, h1').filter({ hasText: /test.*llm/i }); + expect(await pageTitle.isVisible()).toBe(true); + + // Verify LLM Connection section exists + const connectionSection = page.locator('.llm-connection-section'); + expect(await connectionSection.isVisible()).toBe(true); + + // Verify text area for input exists + const textarea = page.locator('textarea').first(); + expect(await textarea.isVisible()).toBe(true); + + // Verify Send button exists + const sendButton = page.locator('button').filter({ hasText: /send/i }); + expect(await sendButton.isVisible()).toBe(true); + + await pageHelper.takeScreenshot('test-llm-page-loaded'); + }); + }); + + test('should load LLM connections in dropdown', async ({ page }) => { + await test.step('Wait for connections to load', async () => { + await testLLMHelper.waitForConnectionsToLoad(); + + // Verify dropdown is visible and clickable + const dropdown = page.locator('.select').first(); + expect(await dropdown.isVisible()).toBe(true); + + await pageHelper.takeScreenshot('test-llm-connections-loaded'); + }); + + await test.step('Open dropdown and verify connections exist', async () => { + const trigger = page.locator('.select__trigger').first(); + await trigger.click(); + + // Wait for options to appear + const options = page.locator('.select__option'); + await options.first().waitFor({ state: 'visible' }); + + const optionCount = await options.count(); + expect(optionCount).toBeGreaterThan(0); + + await pageHelper.takeScreenshot('test-llm-dropdown-options'); + + // Close dropdown + await trigger.click(); + }); + }); + + test('should disable Send button when form is incomplete', async ({ page }) => { + await test.step('Verify Send button is disabled initially', async () => { + await testLLMHelper.waitForConnectionsToLoad(); + + const isDisabled = await testLLMHelper.isSendButtonDisabled(); + expect(isDisabled).toBe(true); + + await pageHelper.takeScreenshot('test-llm-send-disabled-initial'); + }); + + await test.step('Verify Send button disabled with only text', async () => { + await testLLMHelper.fillTestText('Test message without connection'); + + const isDisabled = await testLLMHelper.isSendButtonDisabled(); + expect(isDisabled).toBe(true); + + await pageHelper.takeScreenshot('test-llm-send-disabled-text-only'); + }); + + await test.step('Verify Send button disabled with only connection', async () => { + await testLLMHelper.clearTestText(); + await testLLMHelper.selectFirstAvailableConnection(); + + const isDisabled = await testLLMHelper.isSendButtonDisabled(); + expect(isDisabled).toBe(true); + + await pageHelper.takeScreenshot('test-llm-send-disabled-connection-only'); + }); + }); + + test('should enable Send button when form is complete', async ({ page }) => { + await test.step('Select connection and enter text', async () => { + await testLLMHelper.waitForConnectionsToLoad(); + + // Select first available connection + const connectionName = await testLLMHelper.selectFirstAvailableConnection(); + expect(connectionName).not.toBeNull(); + + // Fill text + await testLLMHelper.fillTestText('What is artificial intelligence?'); + + await pageHelper.takeScreenshot('test-llm-form-complete'); + }); + + await test.step('Verify Send button is enabled', async () => { + const isDisabled = await testLLMHelper.isSendButtonDisabled(); + expect(isDisabled).toBe(false); + + await pageHelper.takeScreenshot('test-llm-send-enabled'); + }); + }); + + test('should validate input text length', async ({ page }) => { + await test.step('Enter text up to max length', async () => { + await testLLMHelper.waitForConnectionsToLoad(); + + // Generate text close to max length (1000 characters) + const longText = 'A'.repeat(950); + await testLLMHelper.fillTestText(longText); + + const charCount = await testLLMHelper.getCharacterCount(); + expect(charCount).toBe(950); + + await pageHelper.takeScreenshot('test-llm-text-length-validation'); + }); + + await test.step('Verify max length enforcement', async () => { + // Try to add more characters beyond max + const maxText = 'A'.repeat(1000); + await testLLMHelper.clearTestText(); + await testLLMHelper.fillTestText(maxText + 'EXTRA'); + + const charCount = await testLLMHelper.getCharacterCount(); + expect(charCount).toBeLessThanOrEqual(1000); + + await pageHelper.takeScreenshot('test-llm-max-length-enforced'); + }); + }); + + test('should send inference request and receive response', async ({ page }) => { + await test.step('Complete form and submit', async () => { + await testLLMHelper.waitForConnectionsToLoad(); + + // Select connection + await testLLMHelper.selectFirstAvailableConnection(); + + // Fill test text + const testMessage = 'Explain machine learning in simple terms.'; + await testLLMHelper.fillTestText(testMessage); + + await pageHelper.takeScreenshot('test-llm-before-send'); + + // Click send + await testLLMHelper.clickSendButton(); + }); + + await test.step('Verify loading state', async () => { + // Check if button shows loading state + const isLoading = await testLLMHelper.verifyLoadingState(); + + if (isLoading) { + await pageHelper.takeScreenshot('test-llm-loading-state'); + } + }); + + await test.step('Wait for and verify response', async () => { + // Wait for inference result (with extended timeout) + try { + await testLLMHelper.waitForInferenceResult(); + + // Get the response content + const result = await testLLMHelper.getInferenceResult(); + + // Verify response is not empty + expect(result.length).toBeGreaterThan(0); + expect(result).not.toBe(''); + + await pageHelper.takeScreenshot('test-llm-response-received'); + } catch (error) { + // If inference fails, check for error message + const hasError = await testLLMHelper.verifyErrorMessage(); + + if (hasError) { + await pageHelper.takeScreenshot('test-llm-inference-error'); + console.log('Inference failed with error message displayed'); + } else { + throw error; + } + } + }); + }); + + test('should handle inference errors gracefully', async ({ page }) => { + await test.step('Setup and simulate error scenario', async () => { + await testLLMHelper.waitForConnectionsToLoad(); + + // Select connection + await testLLMHelper.selectFirstAvailableConnection(); + + // Use problematic input + await testLLMHelper.fillTestText(''); + + await pageHelper.takeScreenshot('test-llm-error-setup'); + }); + + + }); + + test('should clear form after successful submission', async ({ page }) => { + await test.step('Submit inference request', async () => { + await testLLMHelper.waitForConnectionsToLoad(); + + await testLLMHelper.selectFirstAvailableConnection(); + await testLLMHelper.fillTestText('Test message for clear verification'); + + await testLLMHelper.clickSendButton(); + }); + + await test.step('Verify form state after submission', async () => { + // Wait a moment for processing + await page.waitForTimeout(2000); + + // Form should maintain values + const charCount = await testLLMHelper.getCharacterCount(); + + // Just verify the page is still functional + expect(charCount).toBeGreaterThanOrEqual(0); + + await pageHelper.takeScreenshot('test-llm-after-submission'); + }); + }); + +}); + + diff --git a/GUI/tests/login-page.spec.ts b/GUI/tests/login-page.spec.ts new file mode 100644 index 00000000..7e4c9a4e --- /dev/null +++ b/GUI/tests/login-page.spec.ts @@ -0,0 +1,109 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Authentication Flow', () => { + test('should redirect to authentication URL', async ({ page }) => { + // Navigate to the authentication URL from the environment configuration + const authUrl = 'http://localhost:3004/et/dev-auth'; + + await test.step('Navigate to authentication URL', async () => { + await page.goto(authUrl); + }); + + await test.step('Verify page loads successfully', async () => { + // Wait for the page to load + await page.waitForLoadState('networkidle'); + + // Verify we're on the correct URL + expect(page.url()).toContain('localhost:3004'); + expect(page.url()).toContain('/et/dev-auth'); + }); + + await test.step('Verify page title and content', async () => { + // Wait for the page title to load + await page.waitForTimeout(2000); + + // Check that the page has loaded (title should not be empty) + const title = await page.title(); + expect(title).toBeTruthy(); + + // Take a screenshot for debugging purposes + await page.screenshot({ path: 'tests/screenshots/auth-page.png' }); + }); + }); + + test('should handle navigation from main app to auth', async ({ page }) => { + await test.step('Start from main application', async () => { + // First go to the main application + await page.goto('http://localhost:3001'); + }); + + await test.step('Navigate to authentication', async () => { + // Navigate to the authentication URL + await page.goto('http://localhost:3004/et/dev-auth'); + + // Wait for navigation to complete + await page.waitForLoadState('networkidle'); + }); + + await test.step('Verify successful redirect', async () => { + // Verify we're on the auth page + expect(page.url()).toContain('localhost:3004'); + expect(page.url()).toContain('/et/dev-auth'); + + // Take a screenshot + await page.screenshot({ path: 'tests/screenshots/auth-redirect.png' }); + }); + }); + + test('should login with EE30303039914 and redirect to user management', async ({ page }) => { + await test.step('Navigate to authentication page', async () => { + // Go to the Estonian authentication page + await page.goto('http://localhost:3004/et/dev-auth'); + await page.waitForLoadState('networkidle'); + }); + + await test.step('Fill login form and submit', async () => { + // Wait for the form to be ready + await page.waitForTimeout(2000); + + // Find and fill the ID code input field + // The form should accept Estonian ID codes starting with EE + const idInput = await page.locator('input[type="text"], input[placeholder*="user name"], input[placeholder*="sisesta"]').first(); + await idInput.fill('EE30303039914'); + + // Find and click the submit button (sisene means "enter" in Estonian) + const submitButton = await page.locator('button[type="submit"], button:has-text("sisene"), button:has-text("Sisene"), input[type="submit"]').first(); + await submitButton.click(); + + // Wait for the authentication to process + await page.waitForTimeout(20000); + }); + + await test.step('Verify redirect to user management', async () => { + // Wait for navigation to complete + await page.waitForLoadState('networkidle', { timeout: 20000 }); + + // Verify we've been redirected to the user management page + // Based on the App.tsx logic, administrators should be redirected to /user-management + const currentUrl = page.url(); + + // The URL should contain the user management path + expect(currentUrl).toContain('http://localhost:3003/rag-search/user-management'); + + // Take a screenshot for debugging + await page.screenshot({ path: 'tests/screenshots/user-management-redirect.png' }); + + // Verify that we can see user management content + // Look for typical user management elements + const pageContent = await page.textContent('body'); + expect(pageContent).toBeTruthy(); + + // Optional: Check for specific user management page elements + const userManagementTitle = await page.locator('h1, h2, h3').filter({ hasText: /user|kasutaj|management|haldus/i }).first(); + if (await userManagementTitle.isVisible()) { + expect(await userManagementTitle.isVisible()).toBe(true); + } + }); + }); + +}); diff --git a/GUI/tests/logout.spec.ts b/GUI/tests/logout.spec.ts new file mode 100644 index 00000000..3cfc6829 --- /dev/null +++ b/GUI/tests/logout.spec.ts @@ -0,0 +1,110 @@ +import { test, expect } from '@playwright/test'; +import { AuthHelper, PageHelper } from './helpers/test-helpers'; + +test.describe('Logout Functionality', () => { + let authHelper: AuthHelper; + let pageHelper: PageHelper; + + // Setup: Login as admin before each test + test.beforeEach(async ({ page }) => { + authHelper = new AuthHelper(page); + pageHelper = new PageHelper(page); + + await test.step('Login as administrator', async () => { + await authHelper.loginAsAdmin(); + await authHelper.verifyAdminRedirect(); + }); + }); + + test('should logout successfully using logout button', async ({ page }) => { + await test.step('Verify user is logged in and on user management page', async () => { + // Confirm we're on the user management page + expect(page.url()).toContain('user-management'); + + // Verify logout button is visible in header + const logoutButton = await page.locator('button:has-text("Logout")'); + expect(await logoutButton.isVisible()).toBe(true); + + await pageHelper.takeScreenshot('logged-in-state'); + }); + + await test.step('Click logout button', async () => { + await authHelper.logout(); + + // Take screenshot during logout process + await pageHelper.takeScreenshot('logout-in-progress'); + }); + + await test.step('Verify redirect to login page', async () => { + await authHelper.verifyLogoutRedirect(); + + // Take screenshot of final logout state + await pageHelper.takeScreenshot('logout-completed'); + }); + + await test.step('Verify user cannot access protected pages after logout', async () => { + // Try to navigate back to user management page + await page.goto('http://localhost:3001/rag-search/user-management'); + await page.waitForLoadState('networkidle', { timeout: 10000 }); + + // Should be redirected back to auth since user is logged out + const currentUrl = page.url(); + expect(currentUrl).not.toContain('/user-management'); + + // Should be on auth page or redirected there + expect(currentUrl).toContain('localhost:3004'); + + await pageHelper.takeScreenshot('protected-page-access-denied'); + }); + }); + + test('should handle logout from different pages', async ({ page }) => { + await test.step('Navigate to different application page', async () => { + // Try to navigate to LLM connections page if available + await page.goto('http://localhost:3001/rag-search/llm-connections'); + await page.waitForLoadState('networkidle'); + + // Take screenshot of different page + await pageHelper.takeScreenshot('different-page-before-logout'); + }); + + await test.step('Logout from different page', async () => { + // Logout button should be available on all pages with header + const logoutButton = await page.locator('button:has-text("Logout")'); + + if (await logoutButton.isVisible()) { + await authHelper.logout(); + await authHelper.verifyLogoutRedirect(); + + await pageHelper.takeScreenshot('logout-from-different-page'); + } else { + console.log('Logout button not available on this page - test skipped'); + } + }); + }); + + test('should show logout button only when user is authenticated', async ({ page }) => { + await test.step('Verify logout button is visible for authenticated user', async () => { + const logoutButton = await page.locator('button:has-text("Logout")'); + expect(await logoutButton.isVisible()).toBe(true); + + await pageHelper.takeScreenshot('logout-button-visible'); + }); + + await test.step('Logout and verify button is no longer visible', async () => { + await authHelper.logout(); + + // After logout and redirect, logout button should not be visible + await page.waitForTimeout(2000); + + const logoutButton = await page.locator('button:has-text("Logout")'); + + // Should not find logout button on auth page + if (await logoutButton.count() > 0) { + expect(await logoutButton.isVisible()).toBe(false); + } + + await pageHelper.takeScreenshot('logout-button-hidden'); + }); + }); +}); \ No newline at end of file diff --git a/GUI/tests/user-management.spec.ts b/GUI/tests/user-management.spec.ts new file mode 100644 index 00000000..56e6604d --- /dev/null +++ b/GUI/tests/user-management.spec.ts @@ -0,0 +1,441 @@ +import { test, expect } from '@playwright/test'; +import { AuthHelper, UserManagementHelper, PageHelper, TestDataFactory } from './helpers/test-helpers'; + +test.describe('User Management', () => { + let authHelper: AuthHelper; + let userManagementHelper: UserManagementHelper; + let pageHelper: PageHelper; + + // Setup: Login as admin before each test + test.beforeEach(async ({ page }) => { + authHelper = new AuthHelper(page); + userManagementHelper = new UserManagementHelper(page); + pageHelper = new PageHelper(page); + + await test.step('Login as administrator', async () => { + await authHelper.loginAsAdmin(); + await authHelper.verifyAdminRedirect(); + }); + }); + + test('should display user management page with users table', async ({ page }) => { + await test.step('Verify page title and navigation', async () => { + // Check page title + const pageTitle = await page.locator('h1, h2, h3, .title').filter({ hasText: /user|kasutaj|management|haldus/i }).first(); + expect(await pageTitle.isVisible()).toBe(true); + + // Verify add user button is present + const addUserButton = await page.locator('button').filter({ hasText: /add.*user|lisa.*kasutaja/i }); + expect(await addUserButton.isVisible()).toBe(true); + }); + + await test.step('Verify users table is displayed', async () => { + // Wait for table to load + await page.waitForSelector('table, [data-testid="data-table"]', { timeout: 10000 }); + + // Check if table headers are present + const expectedHeaders = ['fullName', 'personalId', 'title', 'role', 'email', 'actions']; + + for (const header of expectedHeaders) { + const headerElement = await page.locator('th').filter({ hasText: new RegExp(header, 'i') }); + // Header might not be visible if table is empty + if (await headerElement.count() > 0) { + expect(await headerElement.first().isVisible()).toBe(true); + } + } + + // Take screenshot for verification + await pageHelper.takeScreenshot('user-management-table'); + }); + + await test.step('Verify pagination and sorting controls', async () => { + // Check for pagination controls + const paginationContainer = await page.locator('[data-testid="pagination"], .pagination, nav[aria-label*="pagination"]'); + if (await paginationContainer.count() > 0) { + expect(await paginationContainer.first().isVisible()).toBe(true); + } + + // Check for sorting capabilities (column headers should be clickable) + const sortableHeaders = await page.locator('th button, th[role="columnheader"]'); + if (await sortableHeaders.count() > 0) { + expect(await sortableHeaders.first().isVisible()).toBe(true); + } + }); + }); + + test('should open and close add user modal', async ({ page }) => { + await test.step('Click add user button', async () => { + await userManagementHelper.openAddUserModal(); + }); + + await test.step('Verify modal is opened', async () => { + // Check for form elements in modal + const formFields = await page.locator('input[type="text"], input[type="email"], select, textarea'); + expect(await formFields.count()).toBeGreaterThan(0); + + // Take screenshot of modal + await pageHelper.takeScreenshot('add-user-modal'); + }); + + await test.step('Close modal', async () => { + await userManagementHelper.closeModal(); + }); + }); + + test('should validate required fields in add user form', async ({ page }) => { + await test.step('Open add user modal', async () => { + const addUserButton = await page.locator('button').filter({ hasText: /add.*user|lisa.*kasutaja/i }).first(); + await addUserButton.click(); + await page.waitForTimeout(1000); + }); + + await test.step('Verify submit button is disabled for empty form', async () => { + // Check that submit button is disabled when form is empty + const submitButton = await page.locator('button:has-text("Confirm")').first(); + + // Button should be disabled for empty form + expect(await submitButton.isEnabled()).toBe(false); + + // Take screenshot showing disabled button + await page.screenshot({ path: 'tests/screenshots/disabled-submit-button.png' }); + }); + + await test.step('Fill partial form to test field validation', async () => { + // Fill only one field to see if button becomes enabled or if individual field validation appears + const nameInput = await page.locator('input[placeholder*="name"], input[name*="name"], input[name*="fullName"]').first(); + if (await nameInput.isVisible()) { + await nameInput.fill('Test'); + + // Check if any validation messages appear for other empty required fields + await page.waitForTimeout(1000); + + // Take screenshot showing partial validation state + await page.screenshot({ path: 'tests/screenshots/partial-form-validation.png' }); + } + }); + + await test.step('Verify required field indicators are present', async () => { + // Check for visual indicators that fields are required + const requiredIndicators = await page.locator('label:has-text("*"), .required, [required], input[placeholder*="required"]'); + + // At least some required field indicators should be visible + if (await requiredIndicators.count() > 0) { + expect(await requiredIndicators.first().isVisible()).toBe(true); + } + + // Take screenshot of validation state + await page.screenshot({ path: 'tests/screenshots/user-form-validation.png' }); + }); + }); + + test('should fill and submit user creation form', async ({ page }) => { + const testUser = TestDataFactory.createTestUser(); + + await test.step('Open add user modal', async () => { + await userManagementHelper.openAddUserModal(); + }); + + await test.step('Fill user form with valid data', async () => { + await userManagementHelper.fillUserForm(testUser); + await pageHelper.takeScreenshot('user-form-filled'); + }); + + await test.step('Submit form', async () => { + await userManagementHelper.submitForm(); + }); + + await test.step('Verify user creation success', async () => { + await userManagementHelper.verifySuccessNotification(/success|created|added/i); + await pageHelper.takeScreenshot('user-creation-success'); + }); + }); + + test('should edit existing user', async ({ page }) => { + await test.step('Find and click edit button for first user', async () => { + // Wait for table to load + await page.waitForSelector('table, [data-testid="data-table"]', { timeout: 10000 }); + + // Find edit button in actions column + const editButton = await page.locator('button[aria-label*="edit"], button:has-text("Change"), .edit-button, [data-testid*="edit"]').first(); + + if (await editButton.isVisible()) { + await editButton.click(); + await page.waitForTimeout(1000); + + // Verify edit modal opened + const modal = await page.locator('[role="dialog"], .modal, .dialog'); + expect(await modal.isVisible()).toBe(true); + + await page.screenshot({ path: 'tests/screenshots/edit-user-modal.png' }); + } else { + // If no users exist to edit, skip this test + console.log('No users available to edit - skipping test'); + return; + } + }); + + await test.step('Modify user data', async () => { + // Update title field + const titleInput = await page.locator('input[name*="title"], input[name*="csaTitle"]').first(); + if (await titleInput.isVisible()) { + await titleInput.clear(); + await titleInput.fill('Updated Test Manager'); + } + + await page.screenshot({ path: 'tests/screenshots/user-form-updated.png' }); + }); + + await test.step('Save changes', async () => { + // The save button is the same "Confirm" button for both create and edit modes + const saveButton = await page.locator('button:has-text("Confirm")').first(); + await saveButton.click(); + + // Wait for save operation + await page.waitForTimeout(3000); + + // Verify success + const successMessage = await page.locator('.toast, .notification, .alert').filter({ hasText: /success|updated|saved/i }); + if (await successMessage.count() > 0) { + expect(await successMessage.first().isVisible()).toBe(true); + } + }); + }); + + + test('should handle table pagination', async ({ page }) => { + await test.step('Test pagination controls', async () => { + // Wait for table to load + await page.waitForSelector('table, [data-testid="data-table"]', { timeout: 10000 }); + + // Look for next page button + const nextButton = await page.locator('button[aria-label*="next"], button:has-text("Next"), .pagination-next'); + + if (await nextButton.count() > 0 && await nextButton.first().isEnabled()) { + await nextButton.first().click(); + await page.waitForTimeout(2000); + await pageHelper.takeScreenshot('pagination-next-page'); + + // Go back to first page + const prevButton = await page.locator('button[aria-label*="previous"], button:has-text("Previous"), .pagination-prev'); + if (await prevButton.count() > 0 && await prevButton.first().isEnabled()) { + await prevButton.first().click(); + await page.waitForTimeout(2000); + } + } + }); + }); + + test('should delete existing user with confirmation', async ({ page }) => { + await test.step('Find and click delete button for first user', async () => { + // Wait for table to load + await page.waitForSelector('table, [data-testid="data-table"]', { timeout: 10000 }); + + // Check if there are users to delete + const userExists = await userManagementHelper.deleteFirstUser(); + + if (userExists) { + // Take screenshot of confirmation dialog + await pageHelper.takeScreenshot('delete-user-confirmation'); + } else { + console.log('No users available to delete - skipping test'); + return; + } + }); + + await test.step('Verify delete confirmation dialog', async () => { + // Check that confirmation dialog has proper content + const dialogContent = await page.locator('[role="dialog"] p, .modal p, .dialog p').first(); + if (await dialogContent.isVisible()) { + const content = await dialogContent.textContent(); + expect(content).toBeTruthy(); + } + + // Verify both Cancel and Confirm buttons are present + const cancelButton = await page.locator('button:has-text("Cancel")'); + const confirmButton = await page.locator('button:has-text("Confirm")'); + + expect(await cancelButton.isVisible()).toBe(true); + expect(await confirmButton.isVisible()).toBe(true); + }); + + await test.step('Confirm deletion', async () => { + await userManagementHelper.confirmDeletion(); + + // Verify success notification + await userManagementHelper.verifySuccessNotification(/success|deleted|removed/i); + + // Take screenshot of success state + await pageHelper.takeScreenshot('user-deletion-success'); + }); + }); + + test('should cancel user deletion', async ({ page }) => { + await test.step('Find and click delete button for first user', async () => { + // Wait for table to load + await page.waitForSelector('table, [data-testid="data-table"]', { timeout: 10000 }); + + // Check if there are users to delete + const userExists = await userManagementHelper.deleteFirstUser(); + + if (!userExists) { + console.log('No users available to delete - skipping test'); + return; + } + }); + + await test.step('Cancel deletion', async () => { + await userManagementHelper.cancelDeletion(); + + // Take screenshot showing canceled state + await pageHelper.takeScreenshot('user-deletion-canceled'); + }); + + await test.step('Verify user still exists in table', async () => { + // The table should still contain users since deletion was canceled + await pageHelper.waitForTable(); + + // Check that we still have table rows with user data + const tableRows = await page.locator('table tbody tr, [data-testid="data-table"] tbody tr'); + if (await tableRows.count() > 0) { + expect(await tableRows.first().isVisible()).toBe(true); + } + }); + }); + + test('should search users by name using search icon', async ({ page }) => { + await test.step('Wait for table to load', async () => { + await pageHelper.waitForTable(); + await pageHelper.takeScreenshot('table-before-search'); + }); + + await test.step('Search for a user by name', async () => { + // Try to search in the name/fullName column + const searchSuccess = await userManagementHelper.searchInColumn('name', 'Test'); + + if (searchSuccess) { + // Take screenshot of search in action + await pageHelper.takeScreenshot('name-column-search-active'); + + // Wait for search results to load + await page.waitForTimeout(2000); + + // Verify search results contain the search term + const resultsValid = await userManagementHelper.verifySearchResults(0, 'Test'); // Assuming name is first column + expect(resultsValid).toBe(true); + + await pageHelper.takeScreenshot('name-search-results'); + } else { + console.log('Search functionality not available in name column - skipping verification'); + } + }); + + await test.step('Clear search and verify table returns to original state', async () => { + await userManagementHelper.clearColumnSearch('name'); + await pageHelper.takeScreenshot('search-cleared'); + }); + }); + + test('should search users by email using search icon', async ({ page }) => { + await test.step('Wait for table to load', async () => { + await pageHelper.waitForTable(); + }); + + await test.step('Search for a user by email', async () => { + // Try to search in the email column + const searchSuccess = await userManagementHelper.searchInColumn('email', '@'); + + if (searchSuccess) { + // Take screenshot of search in action + await pageHelper.takeScreenshot('email-column-search-active'); + + // Wait for search results to load + await page.waitForTimeout(2000); + + // Verify search results contain the search term (assuming email is column 4) + const resultsValid = await userManagementHelper.verifySearchResults(4, '@'); + expect(resultsValid).toBe(true); + + await pageHelper.takeScreenshot('email-search-results'); + } else { + console.log('Search functionality not available in email column - skipping verification'); + } + }); + + await test.step('Clear search', async () => { + await userManagementHelper.clearColumnSearch('email'); + }); + }); + + test('should sort users by name using arrow icons', async ({ page }) => { + await test.step('Wait for table to load', async () => { + await pageHelper.waitForTable(); + await pageHelper.takeScreenshot('table-before-sort'); + }); + + await test.step('Sort by name in ascending order', async () => { + const sortSuccess = await userManagementHelper.sortByArrowIcon('name', 'asc'); + + if (sortSuccess) { + // Wait for sort to complete + await page.waitForTimeout(2000); + + // Take screenshot of sorted table + await pageHelper.takeScreenshot('name-sorted-asc'); + + // Verify the sort worked (check first column) + const sortValid = await userManagementHelper.verifyTableSort(0, 'asc'); + if (sortValid) { + expect(sortValid).toBe(true); + } else { + console.log('Sort verification failed or not enough data to verify'); + } + } else { + console.log('Sort functionality not available - skipping verification'); + } + }); + + await test.step('Sort by name in descending order', async () => { + const sortSuccess = await userManagementHelper.sortByArrowIcon('name', 'desc'); + + if (sortSuccess) { + // Wait for sort to complete + await page.waitForTimeout(2000); + + // Take screenshot of sorted table + await pageHelper.takeScreenshot('name-sorted-desc'); + + // Verify the sort worked + const sortValid = await userManagementHelper.verifyTableSort(0, 'desc'); + if (sortValid) { + expect(sortValid).toBe(true); + } + } + }); + }); + + test('should sort users by role using arrow icons', async ({ page }) => { + await test.step('Wait for table to load', async () => { + await pageHelper.waitForTable(); + }); + + await test.step('Sort by role column', async () => { + const sortSuccess = await userManagementHelper.sortByArrowIcon('role', 'asc'); + + if (sortSuccess) { + // Wait for sort to complete + await page.waitForTimeout(2000); + + // Take screenshot of sorted table + await pageHelper.takeScreenshot('role-sorted'); + + // Verify the sort worked (assuming role is column 3) + const sortValid = await userManagementHelper.verifyTableSort(3, 'asc'); + if (sortValid) { + expect(sortValid).toBe(true); + } + } else { + console.log('Sort functionality not available for role column'); + } + }); + }); +}); \ No newline at end of file