diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..67a1215 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +BASE_URL=https://gxcapture-redbird-dc.galaxe.com:6500 +USERNAME=slogan11 +PASSWORD=Simlaworld@123 diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000..c6eae23 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,50 @@ +name: Playwright Tests +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + inputs: + base_url: + description: 'Base URL for testing' + required: false + default: 'https://gxcapture-redbird-dc.galaxe.com:6500' + +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + run: npm ci + + - name: Install Playwright Browsers + run: npx playwright install --with-deps + + - name: Run Playwright tests + run: npx playwright test + env: + BASE_URL: ${{ github.event.inputs.base_url || secrets.BASE_URL }} + USERNAME: ${{ secrets.TEST_USERNAME }} + PASSWORD: ${{ secrets.TEST_PASSWORD }} + + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 + + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: test-results + path: test-results/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index 008be21..3546d1c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ -/target/ -.project -.classpath -/.settings/ -/ExtentReports/ -logfile.log -/test-output/ - +node_modules/ +dist/ +test-results/ +playwright-report/ +playwright/.auth/ +blob-report/ +.env +*.tsbuildinfo diff --git a/README.md b/README.md index 33ab4ca..60efb86 100644 --- a/README.md +++ b/README.md @@ -1,87 +1,165 @@ -selenium-testng-framework ---- - ---- -A sample framework based on Page Object Model, Selenium, TestNG using Java. - -This framework is based in **Page Object Model (POM).** - -The framework uses: - -1. Java -2. Selenium -3. TestNG -4. ExtentReport -5. Log4j -6. SimpleJavaMail - -Steps to create test cases: ----- -Let's say we want to automate Google search test. - -1.Create GoogleSearchPage in **pages** package. - A page class typically should contain all the elements that are present on the page and corresponding action methods. - - ``` - public class GooglePage extends BasePage { - - @FindBy(name = "q") - private WebElement searchinput; - - public GooglePage(WebDriver driver) { - super(driver); - } - - public void searchText(String key) { - searchinput.sendKeys(key + Keys.ENTER); - } - -} -``` -2.Create the test class which class the methods of GoogleSearchPage - -``` -@Test(testName = "Google search test", description = "Test description") -public class GoogleSearchTest extends BaseTest { - - @Test - public void googleSearchTest() { - driver.get("https://www.google.co.in/"); - GooglePage googlePage = PageinstancesFactory.getInstance(GooglePage.class); - googlePage.searchText("abc"); - Assert.assertTrue(driver.getTitle().contains("abc"), "Title doesn't contain abc : Test Failed"); - } -} -``` -3.Add the test class in testng.xml file under the folder `src/test/resources/suites/` - -``` - - - - - -``` -4.Execute the test cases by maven command `mvn clean test` - ---- - -Reproting ---- -The framework gives report in three ways, - -1. Log - In file `logfile.log`. -2. A html report - Which is generated using extent reports, under the folder `ExtentReports`. -3. A mail report - For which the toggle `mail.sendmail` in `test.properties` should be set `true`. And all the properties such as `smtp host, port, proxy details, etc.,` should be provided correctly. - ---- - -Key Points: ---- - -1. The class `WebDriverContext` is responsible for maintaining the same WebDriver instance throughout the test. So whenever you require a webdriver instance which has been using for current test (In current thread) always call `WebDriverContext.getDriver()`. -2. Always use `PageinstancesFactory.getInstance(type)` to get the instance of particular Page Object. (Of course you can use `new` but it's better use a single approach across the framework. - ---- - ->For any query or suggestions please do comment or mail @ diggavibharathish@gmail.com +# GxCapture Redbird - Playwright Test Automation + +Automated regression test suite for the GxCapture Redbird Benefits Management application using Playwright with TypeScript. + +## Test Coverage + +| Module | Test Cases | Spec File | +|--------|-----------|-----------| +| Portal Screen | TC#1-2 | `tests/portal/portal.spec.ts` | +| BM Landing Page | TC#3 | `tests/portal/bm-landing.spec.ts` | +| Manage Clients | TC#4-5 | `tests/manage-clients/manage-clients.spec.ts` | +| Manage Context | TC#6-7 | `tests/manage-context/manage-context.spec.ts` | +| Manage Definitions | TC#8 | `tests/manage-definitions/manage-definitions.spec.ts` | +| Hierarchy | TC#9 | `tests/hierarchy/hierarchy.spec.ts` | +| Templates | TC#10-13 | `tests/templates/templates.spec.ts` | +| Plans | TC#14-22 | `tests/plans/plans.spec.ts` | +| My Work Queue | TC#23-27 | `tests/my-work-queue/my-work-queue.spec.ts` | +| Roles & Privileges | TC#28-32 | `tests/roles-and-privileges/roles.spec.ts` | +| Plan Life Cycle | TC#33-46 | `tests/plan-lifecycle/plan-lifecycle.spec.ts` | +| Rules | TC#47-57 | `tests/rules/rules.spec.ts` | +| Load Definitions | TC#58-66, 70-71 | `tests/load-definitions/load-definitions.spec.ts` | +| Report Configuration | TC#67-68 | `tests/report-configuration/report-config.spec.ts` | +| Exports | TC#69 | `tests/exports/exports.spec.ts` | + +**Total: 71 test cases automated across 15 modules** + +## Project Structure + +``` +├── pages/ # Page Object Model classes +│ ├── LoginPage.ts +│ ├── PortalPage.ts +│ ├── DashboardPage.ts +│ ├── ManageClientsPage.ts +│ ├── ManageContextPage.ts +│ ├── ManageDefinitionsPage.ts +│ ├── HierarchyPage.ts +│ ├── TemplatesPage.ts +│ ├── PlansPage.ts +│ ├── MyWorkQueuePage.ts +│ ├── RolesAndPrivilegesPage.ts +│ ├── LoadDefinitionsPage.ts +│ ├── ReportConfigurationPage.ts +│ └── ValidationsPage.ts +├── tests/ # Test specifications +│ ├── auth.setup.ts # Authentication setup +│ ├── portal/ +│ ├── manage-clients/ +│ ├── manage-context/ +│ ├── manage-definitions/ +│ ├── hierarchy/ +│ ├── templates/ +│ ├── plans/ +│ ├── my-work-queue/ +│ ├── plan-lifecycle/ +│ ├── rules/ +│ ├── load-definitions/ +│ ├── report-configuration/ +│ └── exports/ +├── utils/ # Helper utilities +│ ├── helpers.ts +│ └── test-config.ts +├── fixtures/ # Test data +│ └── test-data.json +├── playwright.config.ts # Playwright configuration +└── .github/workflows/ # CI/CD pipeline + └── playwright.yml +``` + +## Setup + +### Prerequisites +- Node.js 18+ +- Access to GxCapture Redbird application network + +### Installation + +```bash +# Install dependencies +npm install + +# Install Playwright browsers +npx playwright install --with-deps +``` + +### Configuration + +1. Copy `.env.example` to `.env`: +```bash +cp .env.example .env +``` + +2. Update `.env` with your credentials: +```env +BASE_URL=https://gxcapture-redbird-dc.galaxe.com:6500 +USERNAME=your_username +PASSWORD=your_password +``` + +## Running Tests + +```bash +# Run all tests +npm test + +# Run tests with browser visible +npm run test:headed + +# Run in debug mode (step through tests) +npm run test:debug + +# Run specific module +npm run test:portal +npm run test:plans +npm run test:lifecycle +npm run test:templates +npm run test:clients +npm run test:context +npm run test:definitions +npm run test:hierarchy +npm run test:workqueue +npm run test:roles +npm run test:rules +npm run test:load +npm run test:reports +npm run test:exports + +# View HTML report +npm run report +``` + +## CI/CD + +The GitHub Actions workflow runs automatically on: +- Push to `main` branch +- Pull requests to `main` +- Manual trigger (workflow_dispatch) + +### Required Secrets (GitHub Repository Settings) +- `BASE_URL` - Application base URL +- `TEST_USERNAME` - Test user username +- `TEST_PASSWORD` - Test user password + +## Customization + +### Updating Locators +Since this framework was built without live access to the application UI, some locators may need adjustment. Each Page Object class uses multiple locator strategies (role-based, text-based, CSS selectors) with fallbacks. + +To update locators: +1. Run tests in headed mode: `npm run test:headed` +2. Use Playwright Inspector: `npm run test:debug` +3. Use Playwright Codegen: `npx playwright codegen ` + +### Adding New Test Cases +1. Create or update the appropriate spec file in `tests/` +2. If needed, add new Page Object methods in `pages/` +3. Follow the existing pattern of `test.describe` and `test()` blocks + +## Architecture + +- **Page Object Model (POM)**: Each page/screen has a dedicated class encapsulating locators and actions +- **Authentication**: Shared auth state via `storageState` - login happens once, all tests reuse the session +- **Cross-browser**: Configured for Chromium and Firefox +- **Auto-retry**: Failed tests retry once with trace capture +- **Screenshots/Videos**: Captured automatically on failure diff --git a/fixtures/test-data.json b/fixtures/test-data.json new file mode 100644 index 0000000..f42bf09 --- /dev/null +++ b/fixtures/test-data.json @@ -0,0 +1,45 @@ +{ + "testClients": { + "newClient": { + "name": "AutoTest_Client", + "description": "Client created by automated test" + } + }, + "testContexts": { + "newContext": { + "name": "AutoTest_Context", + "description": "Context for automated testing" + } + }, + "testDefinitions": { + "newDefinition": { + "name": "AutoTest_Definition", + "sourceType": "Excel" + } + }, + "testTemplates": { + "newTemplate": { + "name": "AutoTest_Template" + } + }, + "testPlans": { + "newPlan": { + "name": "AutoTest_Plan", + "context": "Default", + "template": "Default" + } + }, + "testValidations": { + "rule1": { + "condition": "{Mail: Max Amount Due}=\" \"", + "message": "Max Amount cannot be blank" + } + }, + "statusColors": { + "open": "grey", + "pendingReview": "orange", + "approved": "green", + "published": "darkgreen", + "rejected": "red" + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e93ad3b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,125 @@ +{ + "name": "gxcapture-playwright-tests", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gxcapture-playwright-tests", + "version": "1.0.0", + "devDependencies": { + "@playwright/test": "^1.44.0", + "@types/node": "^20.12.0", + "dotenv": "^17.4.2", + "typescript": "^5.4.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", + "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a84de52 --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "gxcapture-playwright-tests", + "version": "1.0.0", + "description": "Playwright test automation for GxCapture Redbird Benefits Management application", + "scripts": { + "test": "npx playwright test", + "test:headed": "npx playwright test --headed", + "test:ui": "npx playwright test --ui", + "test:debug": "npx playwright test --debug", + "test:portal": "npx playwright test tests/portal/", + "test:clients": "npx playwright test tests/manage-clients/", + "test:context": "npx playwright test tests/manage-context/", + "test:definitions": "npx playwright test tests/manage-definitions/", + "test:hierarchy": "npx playwright test tests/hierarchy/", + "test:templates": "npx playwright test tests/templates/", + "test:plans": "npx playwright test tests/plans/", + "test:workqueue": "npx playwright test tests/my-work-queue/", + "test:roles": "npx playwright test tests/roles-and-privileges/", + "test:lifecycle": "npx playwright test tests/plan-lifecycle/", + "test:rules": "npx playwright test tests/rules/", + "test:load": "npx playwright test tests/load-definitions/", + "test:reports": "npx playwright test tests/report-configuration/", + "test:exports": "npx playwright test tests/exports/", + "report": "npx playwright show-report" + }, + "devDependencies": { + "@playwright/test": "^1.44.0", + "@types/node": "^20.12.0", + "dotenv": "^17.4.2", + "typescript": "^5.4.0" + } +} diff --git a/pages/DashboardPage.ts b/pages/DashboardPage.ts new file mode 100644 index 0000000..3db3724 --- /dev/null +++ b/pages/DashboardPage.ts @@ -0,0 +1,46 @@ +import { Page, Locator, expect } from '@playwright/test'; + +export class DashboardPage { + readonly page: Page; + readonly configurationMenu: Locator; + readonly adminMenu: Locator; + readonly plansLink: Locator; + readonly myWorkQueueLink: Locator; + readonly errorMessages: Locator; + + constructor(page: Page) { + this.page = page; + this.configurationMenu = page.getByText('Configuration', { exact: false }).first(); + this.adminMenu = page.getByText('Admin', { exact: false }).first(); + this.plansLink = page.getByText('Plans', { exact: false }).first(); + this.myWorkQueueLink = page.getByText('My Work', { exact: false }).first(); + this.errorMessages = page.locator('.error, [class*="error"], .alert-danger'); + } + + async verifyDashboardLoaded(): Promise { + await this.page.waitForLoadState('domcontentloaded'); + await expect(this.errorMessages).not.toBeVisible(); + } + + async openConfiguration(menuItem: string): Promise { + await this.configurationMenu.click(); + await this.page.getByText(menuItem, { exact: false }).first().click(); + await this.page.waitForLoadState('domcontentloaded'); + } + + async openAdmin(menuItem: string): Promise { + await this.adminMenu.click(); + await this.page.getByText(menuItem, { exact: false }).first().click(); + await this.page.waitForLoadState('domcontentloaded'); + } + + async openPlans(): Promise { + await this.plansLink.click(); + await this.page.waitForLoadState('domcontentloaded'); + } + + async openMyWorkQueue(): Promise { + await this.myWorkQueueLink.click(); + await this.page.waitForLoadState('domcontentloaded'); + } +} diff --git a/pages/HierarchyPage.ts b/pages/HierarchyPage.ts new file mode 100644 index 0000000..f1fa457 --- /dev/null +++ b/pages/HierarchyPage.ts @@ -0,0 +1,64 @@ +import { Page, Locator, expect } from '@playwright/test'; +import { generateUniqueName, readNotificationMessage } from '../utils/helpers'; + +export class HierarchyPage { + readonly page: Page; + readonly addCategoryButton: Locator; + readonly addComponentButton: Locator; + readonly addAttributeButton: Locator; + readonly nameInput: Locator; + readonly saveButton: Locator; + readonly cancelButton: Locator; + readonly hierarchyTree: Locator; + readonly searchBar: Locator; + + constructor(page: Page) { + this.page = page; + this.addCategoryButton = page.getByRole('button', { name: /add category/i }).or( + page.locator('button.fa-plus-circle') + ).first(); + this.addComponentButton = page.getByRole('button', { name: /add component/i }).first(); + this.addAttributeButton = page.getByRole('button', { name: /add attribute/i }).first(); + this.nameInput = page.locator('#name').or(page.locator('input[name="name"]')).first(); + this.saveButton = page.getByRole('button', { name: /^Save$/i }); + this.cancelButton = page.locator('#btnCancel').or(page.getByRole('button', { name: /Cancel/i })).first(); + this.hierarchyTree = page.locator('.tree, [class*="hierarchy"], [class*="tree"]').first(); + this.searchBar = page.locator('#search-bar-0'); + } + + async createCategory(name?: string): Promise { + const categoryName = name || generateUniqueName('Category'); + await this.addCategoryButton.click(); + await expect(this.nameInput).toBeVisible(); + await this.nameInput.fill(categoryName); + await this.saveButton.click(); + await this.page.waitForTimeout(1000); + return categoryName; + } + + async createComponent(parentCategory: string, name?: string): Promise { + const componentName = name || generateUniqueName('Component'); + await this.page.getByText(parentCategory).first().click(); + await this.addComponentButton.click(); + await expect(this.nameInput).toBeVisible(); + await this.nameInput.fill(componentName); + await this.saveButton.click(); + await this.page.waitForTimeout(1000); + return componentName; + } + + async createAttribute(parentComponent: string, name?: string): Promise { + const attributeName = name || generateUniqueName('Attribute'); + await this.page.getByText(parentComponent).first().click(); + await this.addAttributeButton.click(); + await expect(this.nameInput).toBeVisible(); + await this.nameInput.fill(attributeName); + await this.saveButton.click(); + await this.page.waitForTimeout(1000); + return attributeName; + } + + async verifyHierarchyNodeExists(nodeName: string): Promise { + await expect(this.page.getByText(nodeName)).toBeVisible(); + } +} diff --git a/pages/LoadDefinitionsPage.ts b/pages/LoadDefinitionsPage.ts new file mode 100644 index 0000000..bb20199 --- /dev/null +++ b/pages/LoadDefinitionsPage.ts @@ -0,0 +1,111 @@ +import { Page, Locator, expect } from '@playwright/test'; +import { generateUniqueName, readNotificationMessage, visibleValidationMessages } from '../utils/helpers'; + +export class LoadDefinitionsPage { + readonly page: Page; + readonly addButton: Locator; + readonly nameInput: Locator; + readonly saveButton: Locator; + readonly cancelButton: Locator; + readonly loadTable: Locator; + readonly searchBar: Locator; + readonly runButton: Locator; + readonly statusColumn: Locator; + readonly batchProcessButton: Locator; + + constructor(page: Page) { + this.page = page; + this.addButton = page.locator('button.fa-plus-circle').or( + page.getByRole('button', { name: /add|\+/i }) + ).first(); + this.nameInput = page.locator('#name').or(page.locator('input[name="name"]')).first(); + this.saveButton = page.getByRole('button', { name: /^Save$/i }); + this.cancelButton = page.locator('#btnCancel').or(page.getByRole('button', { name: /Cancel/i })).first(); + this.loadTable = page.locator('table').first(); + this.searchBar = page.locator('#search-bar-0'); + this.runButton = page.getByRole('button', { name: /run|execute/i }).first(); + this.statusColumn = page.locator('td[class*="status"], td:nth-child(4)'); + this.batchProcessButton = page.getByRole('button', { name: /batch|process/i }).first(); + } + + async searchLoadDefinitions(name: string): Promise { + await this.searchBar.click(); + await this.searchBar.press('Control+A'); + await this.searchBar.press('Backspace'); + await this.searchBar.pressSequentially(name, { delay: 10 }); + await this.searchBar.press('Enter'); + await this.page.waitForTimeout(500); + } + + async loadDefinitionRows(): Promise { + return this.loadTable.locator('tbody tr').evaluateAll((rows) => + rows.map((row) => [...(row as HTMLTableRowElement).cells].map((cell) => cell.textContent?.trim() || '')) + ); + } + + async createLoadDefinition(name?: string): Promise<{ name: string; message: string }> { + const loadName = name || generateUniqueName('Load'); + await this.addButton.click(); + await expect(this.nameInput).toBeVisible(); + await this.nameInput.fill(loadName); + await this.saveButton.click(); + const message = await readNotificationMessage(this.page); + return { name: loadName, message }; + } + + async openEditForm(name: string): Promise { + await this.searchLoadDefinitions(name); + const row = this.loadTable.locator('tbody tr').filter({ hasText: name }).first(); + await row.locator('button.edit, [title*="edit" i]').first().click(); + await expect(this.nameInput).toBeVisible(); + } + + async deleteLoadDefinition(name: string): Promise<{ deleted: boolean }> { + await this.searchLoadDefinitions(name); + const row = this.loadTable.locator('tbody tr').filter({ hasText: name }).first(); + if (!(await row.isVisible().catch(() => false))) { + return { deleted: false }; + } + await row.locator('input[type="checkbox"]').first().check(); + const deleteButton = this.page.locator('button.tableRowDelete').or( + this.page.getByRole('button', { name: /delete/i }) + ).first(); + const dialogPromise = this.page.waitForEvent('dialog', { timeout: 2_000 }) + .then(async (dialog) => { await dialog.accept(); return dialog.message(); }) + .catch(() => ''); + await deleteButton.click(); + await dialogPromise; + const confirmBtn = this.page.getByRole('button', { name: /^(Yes|OK|Confirm|Delete)$/i }).last(); + if (await confirmBtn.isVisible({ timeout: 3_000 }).catch(() => false)) { + await confirmBtn.click(); + } + await this.page.waitForTimeout(1000); + return { deleted: true }; + } + + async verifyValidationMessages(): Promise { + return visibleValidationMessages(this.page); + } + + async runLoadDefinition(name: string): Promise { + await this.searchLoadDefinitions(name); + const row = this.loadTable.locator('tbody tr').filter({ hasText: name }).first(); + await row.locator('button.run, [title*="run" i], button.fa-play').first().click(); + return readNotificationMessage(this.page); + } + + async batchProcess(): Promise { + await this.batchProcessButton.click(); + return readNotificationMessage(this.page); + } + + async waitForLoadDefinitionRow(name: string): Promise { + await this.searchLoadDefinitions(name); + await expect.poll(async () => { + const rows = await this.loadDefinitionRows(); + return rows.some((row) => row.some((cell) => cell.includes(name))); + }, { message: `Load definition "${name}" should be visible`, timeout: 15_000 }).toBe(true); + const rows = await this.loadDefinitionRows(); + return rows.find((row) => row.some((cell) => cell.includes(name))); + } +} diff --git a/pages/LoginPage.ts b/pages/LoginPage.ts new file mode 100644 index 0000000..c7c851c --- /dev/null +++ b/pages/LoginPage.ts @@ -0,0 +1,52 @@ +import { Page, Locator, expect } from '@playwright/test'; +import { APP_URL } from '../utils/test-config'; + +export class LoginPage { + readonly page: Page; + readonly signInHeading: Locator; + readonly usernameInput: Locator; + readonly passwordInput: Locator; + readonly loginButton: Locator; + readonly welcomeMessage: Locator; + readonly profileDropdown: Locator; + readonly logoutMenuItem: Locator; + + constructor(page: Page) { + this.page = page; + this.signInHeading = page.getByRole('heading', { name: 'Sign In' }); + this.usernameInput = page.getByRole('textbox', { name: 'Username' }); + this.passwordInput = page.getByRole('textbox', { name: 'Password' }); + this.loginButton = page.getByRole('button', { name: /Login/i }); + this.welcomeMessage = page.getByText('What would you like to work on today?'); + this.profileDropdown = page.locator('#dropdown-size-small'); + this.logoutMenuItem = page.getByRole('menuitem', { name: /Log Out/i }); + } + + async goto(): Promise { + await this.page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 45_000 }); + await expect(this.signInHeading).toBeVisible(); + } + + async login(username: string, password: string): Promise { + await this.usernameInput.fill(username); + await this.passwordInput.fill(password); + await this.loginButton.click(); + await this.page.waitForURL(/\/portal#\/home$/, { timeout: 30_000 }); + await expect(this.welcomeMessage).toBeVisible(); + } + + async logout(): Promise { + await this.profileDropdown.click(); + await this.logoutMenuItem.click(); + await this.page.waitForURL(/\/portal#\/$/, { timeout: 15_000 }); + await expect(this.signInHeading).toBeVisible(); + await expect(this.usernameInput).toBeVisible(); + } + + async verifyLoginPage(): Promise { + await expect(this.signInHeading).toBeVisible(); + await expect(this.usernameInput).toBeVisible(); + await expect(this.passwordInput).toBeVisible(); + await expect(this.loginButton).toBeVisible(); + } +} diff --git a/pages/ManageClientsPage.ts b/pages/ManageClientsPage.ts new file mode 100644 index 0000000..7ff2f55 --- /dev/null +++ b/pages/ManageClientsPage.ts @@ -0,0 +1,56 @@ +import { Page, Locator, expect } from '@playwright/test'; +import { generateUniqueName, readNotificationMessage, searchTable } from '../utils/helpers'; + +export class ManageClientsPage { + readonly page: Page; + readonly addButton: Locator; + readonly nameInput: Locator; + readonly saveButton: Locator; + readonly clientTable: Locator; + readonly searchBar: Locator; + + constructor(page: Page) { + this.page = page; + this.addButton = page.locator('button.fa-plus-circle').or( + page.getByRole('button', { name: /add|\+/i }) + ).first(); + this.nameInput = page.locator('#name').or( + page.locator('input[name*="name"]') + ).first(); + this.saveButton = page.getByRole('button', { name: /^Save$/i }); + this.clientTable = page.locator('table').filter({ hasText: 'Client' }).first(); + this.searchBar = page.locator('#search-bar-0'); + } + + async addNewClient(clientName?: string): Promise { + const name = clientName || generateUniqueName('Client'); + await this.addButton.click(); + await expect(this.nameInput).toBeVisible(); + await this.nameInput.fill(name); + await this.saveButton.click(); + await this.page.waitForTimeout(1000); + return name; + } + + async editClient(clientName: string, newName: string): Promise { + await searchTable(this.page, clientName); + const row = this.clientTable.locator('tbody tr').filter({ hasText: clientName }).first(); + await row.locator('button.edit').or(row.locator('[title*="edit" i]')).first().click(); + await expect(this.nameInput).toBeVisible(); + await this.nameInput.clear(); + await this.nameInput.fill(newName); + await this.saveButton.click(); + await this.page.waitForTimeout(1000); + } + + async clientRows(): Promise { + return this.clientTable.locator('tbody tr').evaluateAll((rows) => + rows.map((row) => [...(row as HTMLTableRowElement).cells].map((cell) => cell.textContent?.trim() || '')) + ); + } + + async verifyClientExists(clientName: string): Promise { + await searchTable(this.page, clientName); + await expect(this.page.getByText(clientName)).toBeVisible(); + } +} diff --git a/pages/ManageContextPage.ts b/pages/ManageContextPage.ts new file mode 100644 index 0000000..e913264 --- /dev/null +++ b/pages/ManageContextPage.ts @@ -0,0 +1,50 @@ +import { Page, Locator, expect } from '@playwright/test'; +import { generateUniqueName, readNotificationMessage, searchTable } from '../utils/helpers'; + +export class ManageContextPage { + readonly page: Page; + readonly addButton: Locator; + readonly nameInput: Locator; + readonly saveButton: Locator; + readonly contextTable: Locator; + readonly cancelButton: Locator; + + constructor(page: Page) { + this.page = page; + this.addButton = page.locator('button.fa-plus-circle').or( + page.getByRole('button', { name: /add|\+/i }) + ).first(); + this.nameInput = page.locator('#name').or( + page.locator('input[name*="name"]') + ).first(); + this.saveButton = page.getByRole('button', { name: /^Save$/i }); + this.contextTable = page.locator('table').first(); + this.cancelButton = page.locator('#btnCancel').or( + page.getByRole('button', { name: /Cancel/i }) + ).first(); + } + + async addNewContext(contextName?: string): Promise { + const name = contextName || generateUniqueName('Context'); + await this.addButton.click(); + await expect(this.nameInput).toBeVisible(); + await this.nameInput.fill(name); + await this.saveButton.click(); + await this.page.waitForTimeout(1000); + return name; + } + + async editContext(contextName: string, newName: string): Promise { + const row = this.contextTable.locator('tbody tr').filter({ hasText: contextName }).first(); + await row.locator('button.edit').or(row.locator('[title*="edit" i]')).first().click(); + await expect(this.nameInput).toBeVisible(); + await this.nameInput.clear(); + await this.nameInput.fill(newName); + await this.saveButton.click(); + await this.page.waitForTimeout(1000); + } + + async verifyContextExists(contextName: string): Promise { + await expect(this.page.getByText(contextName)).toBeVisible(); + } +} diff --git a/pages/ManageDefinitionsPage.ts b/pages/ManageDefinitionsPage.ts new file mode 100644 index 0000000..0229510 --- /dev/null +++ b/pages/ManageDefinitionsPage.ts @@ -0,0 +1,121 @@ +import { Page, Locator, expect } from '@playwright/test'; +import { generateUniqueName, readNotificationMessage, visibleValidationMessages } from '../utils/helpers'; + +export class ManageDefinitionsPage { + readonly page: Page; + readonly addButton: Locator; + readonly nameInput: Locator; + readonly labelInput: Locator; + readonly xmlNodeInput: Locator; + readonly descriptionInput: Locator; + readonly activeCheckbox: Locator; + readonly orderInput: Locator; + readonly saveButton: Locator; + readonly cancelButton: Locator; + readonly definitionTable: Locator; + readonly questionnaireForm: Locator; + readonly searchBar: Locator; + + constructor(page: Page) { + this.page = page; + this.addButton = page.locator('button.fa-plus-circle').or( + page.getByRole('button', { name: /add|\+/i }) + ).first(); + this.nameInput = page.locator('#name').or(page.locator('input[name="name"]')).first(); + this.labelInput = page.locator('#label').or(page.locator('input[name="label"]')).first(); + this.xmlNodeInput = page.locator('#xmlNode').or(page.locator('input[name="xmlNode"]')).first(); + this.descriptionInput = page.locator('#description').or(page.locator('textarea[name="description"]')).first(); + this.activeCheckbox = page.locator('#active').or(page.locator('input[name="active"]')).first(); + this.orderInput = page.locator('#order').or(page.locator('input[name="order"]')).first(); + this.saveButton = page.getByRole('button', { name: /^Save$/i }); + this.cancelButton = page.locator('#btnCancel').or(page.getByRole('button', { name: /Cancel/i })).first(); + this.definitionTable = page.locator('table').first(); + this.questionnaireForm = page.locator('#questionnaire'); + this.searchBar = page.locator('#search-bar-0'); + } + + async openAddForm(): Promise { + await this.addButton.click(); + await expect(this.nameInput).toBeVisible(); + } + + async fillForm(definition: { + name: string; + label: string; + xmlNode: string; + description?: string; + active?: boolean; + order?: string; + }): Promise { + await this.nameInput.fill(definition.name); + await this.labelInput.fill(definition.label); + await this.xmlNodeInput.fill(definition.xmlNode); + if (definition.description) { + await this.descriptionInput.fill(definition.description); + } + if (definition.order) { + await this.orderInput.fill(definition.order); + } + } + + async save(): Promise { + await this.saveButton.click(); + return readNotificationMessage(this.page); + } + + async clickSaveAndReadMessage(): Promise { + await this.saveButton.click(); + return readNotificationMessage(this.page); + } + + async getValidationMessages(): Promise { + return visibleValidationMessages(this.page); + } + + async searchDefinitions(name: string): Promise { + await this.searchBar.click(); + await this.searchBar.press('Control+A'); + await this.searchBar.press('Backspace'); + await this.searchBar.pressSequentially(name, { delay: 10 }); + await this.searchBar.press('Enter'); + await this.page.waitForTimeout(500); + } + + async definitionRows(): Promise { + return this.definitionTable.locator('tbody tr').evaluateAll((rows) => + rows.map((row) => [...(row as HTMLTableRowElement).cells].map((cell) => cell.textContent?.trim() || '')) + ); + } + + async openEditForm(name: string): Promise { + await this.searchDefinitions(name); + const row = this.definitionTable.locator('tbody tr').filter({ hasText: name }).first(); + await row.locator('button.edit').or(row.locator('[title*="edit" i]')).first().click(); + } + + async deleteDefinition(name: string): Promise<{ deleted: boolean; dialogMessage: string; message: string }> { + await this.searchDefinitions(name); + const row = this.definitionTable.locator('tbody tr').filter({ hasText: name }).first(); + if (!(await row.isVisible().catch(() => false))) { + return { deleted: false, dialogMessage: '', message: '' }; + } + await row.locator('input[type="checkbox"]').first().check(); + const deleteButton = this.page.locator('button.tableRowDelete').or( + this.page.getByRole('button', { name: /delete/i }) + ).first(); + const dialogPromise = this.page.waitForEvent('dialog', { timeout: 2_000 }) + .then(async (dialog) => { await dialog.accept(); return dialog.message(); }) + .catch(() => ''); + await deleteButton.click(); + const dialogMessage = await dialogPromise; + if (!dialogMessage) { + const confirmBtn = this.page.getByRole('button', { name: /^(Yes|OK|Confirm|Delete)$/i }).last(); + if (await confirmBtn.isVisible({ timeout: 3_000 }).catch(() => false)) { + await confirmBtn.click(); + } + } + await this.page.waitForTimeout(1000); + const message = await readNotificationMessage(this.page); + return { deleted: true, dialogMessage, message }; + } +} diff --git a/pages/MyWorkQueuePage.ts b/pages/MyWorkQueuePage.ts new file mode 100644 index 0000000..68e2344 --- /dev/null +++ b/pages/MyWorkQueuePage.ts @@ -0,0 +1,69 @@ +import { Page, Locator, expect } from '@playwright/test'; + +export class MyWorkQueuePage { + readonly page: Page; + readonly workQueueTable: Locator; + readonly searchBar: Locator; + readonly statusFilter: Locator; + readonly contextFilter: Locator; + readonly pageSizeDropdown: Locator; + + constructor(page: Page) { + this.page = page; + this.workQueueTable = page.locator('table').first(); + this.searchBar = page.locator('#search-bar-0'); + this.statusFilter = page.locator('select[name*="status"], #statusFilter').first(); + this.contextFilter = page.locator('select[name*="context"], #contextFilter').first(); + this.pageSizeDropdown = page.locator('#dropdown-size-small').first(); + } + + async searchWorkQueue(name: string): Promise { + await this.searchBar.click(); + await this.searchBar.press('Control+A'); + await this.searchBar.press('Backspace'); + await this.searchBar.pressSequentially(name, { delay: 10 }); + await this.searchBar.press('Enter'); + await this.page.waitForTimeout(500); + } + + async workQueueRows(): Promise { + return this.workQueueTable.locator('tbody tr').evaluateAll((rows) => + rows.map((row) => [...(row as HTMLTableRowElement).cells].map((cell) => cell.textContent?.trim() || '')) + ); + } + + async verifyWorkQueueLoaded(): Promise { + await expect(this.workQueueTable).toBeVisible(); + } + + async openPlanFromQueue(planName: string): Promise { + await this.searchWorkQueue(planName); + const row = this.workQueueTable.locator('tbody tr').filter({ hasText: planName }).first(); + await row.locator('a, button.edit, [title*="edit" i]').first().click(); + await this.page.waitForLoadState('domcontentloaded'); + } + + async filterByStatus(status: string): Promise { + await this.statusFilter.selectOption(status); + await this.page.waitForTimeout(500); + } + + async filterByContext(contextName: string): Promise { + await this.contextFilter.selectOption(contextName); + await this.page.waitForTimeout(500); + } + + async verifyPlanInQueue(planName: string): Promise { + await this.searchWorkQueue(planName); + const rows = await this.workQueueRows(); + return rows.find((row) => row.some((cell) => cell.includes(planName))); + } + + async verifyPlanNotInQueue(planName: string): Promise { + await this.searchWorkQueue(planName); + await expect.poll(async () => { + const rows = await this.workQueueRows(); + return rows.some((row) => row.some((cell) => cell.includes(planName))); + }, { message: `Plan "${planName}" should not be in work queue`, timeout: 10_000 }).toBe(false); + } +} diff --git a/pages/PlansPage.ts b/pages/PlansPage.ts new file mode 100644 index 0000000..ed06d2e --- /dev/null +++ b/pages/PlansPage.ts @@ -0,0 +1,177 @@ +import { Page, Locator, expect } from '@playwright/test'; +import { generateUniqueName, readNotificationMessage } from '../utils/helpers'; + +export class PlansPage { + readonly page: Page; + readonly addButton: Locator; + readonly saveButton: Locator; + readonly planTable: Locator; + readonly searchBar: Locator; + readonly deleteButton: Locator; + readonly exportButton: Locator; + readonly massStatusButton: Locator; + readonly advanceSearchButton: Locator; + readonly contextFilter: Locator; + + constructor(page: Page) { + this.page = page; + this.addButton = page.locator('button.fa-plus-circle').or( + page.getByRole('button', { name: /add|\+/i }) + ).first(); + this.saveButton = page.getByRole('button', { name: /^Save$/i }); + this.planTable = page.locator('table').first(); + this.searchBar = page.locator('#search-bar-0'); + this.deleteButton = page.locator('button.tableRowDelete').or( + page.getByRole('button', { name: /delete/i }) + ).first(); + this.exportButton = page.getByRole('button', { name: /export/i }).first(); + this.massStatusButton = page.getByRole('button', { name: /mass status|bulk/i }).first(); + this.advanceSearchButton = page.getByRole('button', { name: /advance|advanced/i }).first(); + this.contextFilter = page.locator('select[name*="context"], #contextFilter').first(); + } + + async searchPlans(name: string): Promise { + await this.searchBar.click(); + await this.searchBar.press('Control+A'); + await this.searchBar.press('Backspace'); + await this.searchBar.pressSequentially(name, { delay: 10 }); + await this.searchBar.press('Enter'); + await this.page.waitForTimeout(500); + } + + async planRows(): Promise { + return this.planTable.locator('tbody tr').evaluateAll((rows) => + rows.map((row) => [...(row as HTMLTableRowElement).cells].map((cell) => cell.textContent?.trim() || '')) + ); + } + + async createPlan(planName?: string): Promise<{ name: string; message: string }> { + const name = planName || generateUniqueName('Plan'); + await this.addButton.click(); + await this.page.waitForLoadState('domcontentloaded'); + const nameInput = this.page.locator('#name, #planName, input[name="name"]').first(); + if (await nameInput.isVisible().catch(() => false)) { + await nameInput.fill(name); + } + await this.saveButton.click(); + const message = await readNotificationMessage(this.page); + return { name, message }; + } + + async editPlan(planName: string): Promise { + await this.searchPlans(planName); + const row = this.planTable.locator('tbody tr').filter({ hasText: planName }).first(); + await row.locator('button.edit, [title*="edit" i]').first().click(); + await this.page.waitForLoadState('domcontentloaded'); + } + + async copyPlan(planName: string): Promise { + await this.searchPlans(planName); + const row = this.planTable.locator('tbody tr').filter({ hasText: planName }).first(); + await row.locator('button.copy, [title*="copy" i]').first().click(); + await this.saveButton.click(); + return readNotificationMessage(this.page); + } + + async createNewVersion(planName: string): Promise { + await this.searchPlans(planName); + const row = this.planTable.locator('tbody tr').filter({ hasText: planName }).first(); + await row.locator('button.version, [title*="version" i]').first().click(); + await this.saveButton.click(); + return readNotificationMessage(this.page); + } + + async deletePlan(planName: string): Promise { + await this.searchPlans(planName); + const row = this.planTable.locator('tbody tr').filter({ hasText: planName }).first(); + await row.locator('input[type="checkbox"], input.selection-input-4').first().check(); + const dialogPromise = this.page.waitForEvent('dialog', { timeout: 2_000 }) + .then(async (dialog) => { await dialog.accept(); return dialog.message(); }) + .catch(() => ''); + await this.deleteButton.click(); + await dialogPromise; + const confirmBtn = this.page.getByRole('button', { name: /^(Yes|OK|Confirm|Delete)$/i }).last(); + if (await confirmBtn.isVisible({ timeout: 3_000 }).catch(() => false)) { + await confirmBtn.click(); + } + await this.page.waitForTimeout(1000); + } + + async filterByStatus(status: string): Promise { + await this.page.getByText(status, { exact: true }).first().click(); + await this.page.waitForTimeout(500); + } + + async filterByContext(contextName: string): Promise { + await this.contextFilter.selectOption(contextName); + await this.page.waitForTimeout(500); + } + + async advanceSearch(searchTerm: string): Promise { + await this.advanceSearchButton.click(); + const searchInput = this.page.locator('input[type="search"], input[placeholder*="search" i]').first(); + await searchInput.fill(searchTerm); + await this.page.getByRole('button', { name: /search|apply/i }).first().click(); + await this.page.waitForTimeout(500); + } + + async submitForReview(): Promise { + await this.page.getByRole('button', { name: /submit for review/i }).first().click(); + const commentInput = this.page.locator('textarea, input[name*="comment"]').first(); + if (await commentInput.isVisible({ timeout: 2_000 }).catch(() => false)) { + await commentInput.fill('Automated test - submit for review'); + await this.page.getByRole('button', { name: /submit|confirm|ok/i }).first().click(); + } + return readNotificationMessage(this.page); + } + + async approve(): Promise { + await this.page.getByRole('button', { name: /approve/i }).first().click(); + const commentInput = this.page.locator('textarea, input[name*="comment"]').first(); + if (await commentInput.isVisible({ timeout: 2_000 }).catch(() => false)) { + await commentInput.fill('Automated test - approved'); + await this.page.getByRole('button', { name: /submit|confirm|ok/i }).first().click(); + } + return readNotificationMessage(this.page); + } + + async publish(): Promise { + await this.page.getByRole('button', { name: /publish/i }).first().click(); + const commentInput = this.page.locator('textarea, input[name*="comment"]').first(); + if (await commentInput.isVisible({ timeout: 2_000 }).catch(() => false)) { + await commentInput.fill('Automated test - published'); + await this.page.getByRole('button', { name: /submit|confirm|ok/i }).first().click(); + } + return readNotificationMessage(this.page); + } + + async reject(): Promise { + await this.page.getByRole('button', { name: /reject/i }).first().click(); + const commentInput = this.page.locator('textarea, input[name*="comment"]').first(); + if (await commentInput.isVisible({ timeout: 2_000 }).catch(() => false)) { + await commentInput.fill('Automated test - rejected'); + await this.page.getByRole('button', { name: /submit|confirm|ok/i }).first().click(); + } + return readNotificationMessage(this.page); + } + + async getStatusDot(planName: string): Promise { + const row = this.planTable.locator('tbody tr').filter({ hasText: planName }).first(); + return row.locator('.status-dot, [class*="status"], .dot').first(); + } + + async massStatusUpdate(): Promise { + await this.massStatusButton.click(); + await this.page.waitForLoadState('domcontentloaded'); + } + + async exportPlans(format: string): Promise { + await this.exportButton.click(); + await this.page.getByText(format, { exact: false }).first().click(); + const proceedBtn = this.page.getByRole('button', { name: /proceed|export|download/i }).first(); + if (await proceedBtn.isVisible({ timeout: 2_000 }).catch(() => false)) { + await proceedBtn.click(); + } + await this.page.waitForTimeout(2000); + } +} diff --git a/pages/PortalPage.ts b/pages/PortalPage.ts new file mode 100644 index 0000000..f6b912f --- /dev/null +++ b/pages/PortalPage.ts @@ -0,0 +1,48 @@ +import { Page, Locator, expect } from '@playwright/test'; +import { portalUrl } from '../utils/test-config'; + +export class PortalPage { + readonly page: Page; + readonly gxClientLink: Locator; + readonly benefitsManagementCard: Locator; + readonly welcomeMessage: Locator; + + constructor(page: Page) { + this.page = page; + this.gxClientLink = page.getByRole('link', { name: 'Gx Client' }); + this.benefitsManagementCard = page.getByText('Benefits Management', { exact: false }).first(); + this.welcomeMessage = page.getByText('What would you like to work on today?'); + } + + async goto(): Promise { + await this.page.goto(portalUrl('/portal#/home'), { waitUntil: 'domcontentloaded', timeout: 45_000 }); + await this.page.waitForURL(/\/portal#\/home$/, { timeout: 15_000 }); + } + + async navigateToUserManagement(): Promise { + await this.gxClientLink.click(); + await this.page.waitForURL(/\/portal#\/manageUser$/, { timeout: 15_000 }); + await expect(this.page.getByRole('heading', { name: /Manage Users/i })).toBeVisible(); + } + + async clickBenefitsManagement(): Promise { + const [benefitsPage] = await Promise.all([ + this.page.context().waitForEvent('page', { timeout: 30_000 }).catch(() => this.page), + this.benefitsManagementCard.click(), + ]); + await benefitsPage.waitForLoadState('domcontentloaded'); + return benefitsPage; + } + + async verifyNoSRPInstance(): Promise { + await expect(this.page.getByText('SRP', { exact: true })).not.toBeVisible(); + } + + async verifyNoM3PInstance(): Promise { + await expect(this.page.getByText('M3P', { exact: true })).not.toBeVisible(); + } + + async verifyPortalHome(): Promise { + await expect(this.welcomeMessage).toBeVisible(); + } +} diff --git a/pages/ReportConfigurationPage.ts b/pages/ReportConfigurationPage.ts new file mode 100644 index 0000000..2432c8b --- /dev/null +++ b/pages/ReportConfigurationPage.ts @@ -0,0 +1,64 @@ +import { Page, Locator, expect } from '@playwright/test'; +import { generateUniqueName, readNotificationMessage } from '../utils/helpers'; + +export class ReportConfigurationPage { + readonly page: Page; + readonly addButton: Locator; + readonly nameInput: Locator; + readonly saveButton: Locator; + readonly cancelButton: Locator; + readonly reportTable: Locator; + readonly searchBar: Locator; + + constructor(page: Page) { + this.page = page; + this.addButton = page.locator('button.fa-plus-circle').or( + page.getByRole('button', { name: /add|\+/i }) + ).first(); + this.nameInput = page.locator('#name').or(page.locator('input[name="name"]')).first(); + this.saveButton = page.getByRole('button', { name: /^Save$/i }); + this.cancelButton = page.locator('#btnCancel').or(page.getByRole('button', { name: /Cancel/i })).first(); + this.reportTable = page.locator('table').first(); + this.searchBar = page.locator('#search-bar-0'); + } + + async createReportConfig(name?: string): Promise<{ name: string; message: string }> { + const reportName = name || generateUniqueName('Report'); + await this.addButton.click(); + await expect(this.nameInput).toBeVisible(); + await this.nameInput.fill(reportName); + await this.saveButton.click(); + const message = await readNotificationMessage(this.page); + return { name: reportName, message }; + } + + async editReportConfig(name: string, newName: string): Promise { + const row = this.reportTable.locator('tbody tr').filter({ hasText: name }).first(); + await row.locator('button.edit, [title*="edit" i]').first().click(); + await expect(this.nameInput).toBeVisible(); + await this.nameInput.clear(); + await this.nameInput.fill(newName); + await this.saveButton.click(); + return readNotificationMessage(this.page); + } + + async searchReports(name: string): Promise { + await this.searchBar.click(); + await this.searchBar.press('Control+A'); + await this.searchBar.press('Backspace'); + await this.searchBar.pressSequentially(name, { delay: 10 }); + await this.searchBar.press('Enter'); + await this.page.waitForTimeout(500); + } + + async reportRows(): Promise { + return this.reportTable.locator('tbody tr').evaluateAll((rows) => + rows.map((row) => [...(row as HTMLTableRowElement).cells].map((cell) => cell.textContent?.trim() || '')) + ); + } + + async verifyReportExists(name: string): Promise { + await this.searchReports(name); + await expect(this.page.getByText(name)).toBeVisible(); + } +} diff --git a/pages/RolesAndPrivilegesPage.ts b/pages/RolesAndPrivilegesPage.ts new file mode 100644 index 0000000..43c2e0f --- /dev/null +++ b/pages/RolesAndPrivilegesPage.ts @@ -0,0 +1,55 @@ +import { Page, Locator, expect } from '@playwright/test'; + +export class RolesAndPrivilegesPage { + readonly page: Page; + readonly rolesTable: Locator; + readonly addButton: Locator; + readonly nameInput: Locator; + readonly saveButton: Locator; + readonly searchBar: Locator; + + constructor(page: Page) { + this.page = page; + this.rolesTable = page.locator('table').first(); + this.addButton = page.locator('button.fa-plus-circle').or( + page.getByRole('button', { name: /add|\+/i }) + ).first(); + this.nameInput = page.locator('#name').or(page.locator('input[name="name"]')).first(); + this.saveButton = page.getByRole('button', { name: /^Save$/i }); + this.searchBar = page.locator('#search-bar-0'); + } + + async verifyRolesLoaded(): Promise { + await expect(this.rolesTable).toBeVisible(); + } + + async roleRows(): Promise { + return this.rolesTable.locator('tbody tr').evaluateAll((rows) => + rows.map((row) => [...(row as HTMLTableRowElement).cells].map((cell) => cell.textContent?.trim() || '')) + ); + } + + async verifyRoleExists(roleName: string): Promise { + const rows = await this.roleRows(); + return rows.find((row) => row.some((cell) => cell.includes(roleName))); + } + + async verifyUserRole(username: string, expectedRole: string): Promise { + await expect(this.page.getByText(expectedRole)).toBeVisible(); + } + + async verifyPermission(permission: string): Promise { + const permissionLocator = this.page.getByText(permission, { exact: false }); + return permissionLocator.isVisible(); + } + + async verifyCheckboxState(permission: string, expected: boolean): Promise { + const row = this.rolesTable.locator('tbody tr').filter({ hasText: permission }).first(); + const checkbox = row.locator('input[type="checkbox"]').first(); + if (expected) { + await expect(checkbox).toBeChecked(); + } else { + await expect(checkbox).not.toBeChecked(); + } + } +} diff --git a/pages/TemplatesPage.ts b/pages/TemplatesPage.ts new file mode 100644 index 0000000..002f4fd --- /dev/null +++ b/pages/TemplatesPage.ts @@ -0,0 +1,129 @@ +import { Page, Locator, expect } from '@playwright/test'; +import { generateUniqueName, readNotificationMessage } from '../utils/helpers'; + +export class TemplatesPage { + readonly page: Page; + readonly addButton: Locator; + readonly nameInput: Locator; + readonly descriptionInput: Locator; + readonly templateCodeInput: Locator; + readonly benefitDefinitionDropdown: Locator; + readonly saveButton: Locator; + readonly cancelButton: Locator; + readonly templateTable: Locator; + readonly searchBar: Locator; + + constructor(page: Page) { + this.page = page; + this.addButton = page.locator('button.fa-plus-circle').or( + page.getByRole('button', { name: /add|\+/i }) + ).first(); + this.nameInput = page.locator('#name').or(page.locator('input[name="name"]')).first(); + this.descriptionInput = page.locator('#description').or(page.locator('textarea[name="description"]')).first(); + this.templateCodeInput = page.locator('#templateCode').or(page.locator('input[name="templateCode"]')).first(); + this.benefitDefinitionDropdown = page.locator('#benefitDefinition').or( + page.locator('select[name="benefitDefinition"]') + ).first(); + this.saveButton = page.getByRole('button', { name: /^Save$/i }); + this.cancelButton = page.locator('#btnCancel').or(page.getByRole('button', { name: /Cancel/i })).first(); + this.templateTable = page.locator('table').first(); + this.searchBar = page.locator('#search-bar-0'); + } + + async searchTemplates(name: string): Promise { + await this.searchBar.click(); + await this.searchBar.press('Control+A'); + await this.searchBar.press('Backspace'); + await this.searchBar.pressSequentially(name, { delay: 10 }); + await this.searchBar.press('Enter'); + await this.page.waitForTimeout(500); + } + + async templateRows(): Promise { + return this.templateTable.locator('tbody tr').evaluateAll((rows) => + rows.map((row) => [...(row as HTMLTableRowElement).cells].map((cell) => cell.textContent?.trim() || '')) + ); + } + + async waitForTemplateRow(templateName: string): Promise { + await this.searchTemplates(templateName); + await expect.poll(async () => { + const rows = await this.templateRows(); + return rows.some((row) => row.some((cell) => cell.includes(templateName))); + }, { message: `Template "${templateName}" should be visible`, timeout: 15_000 }).toBe(true); + const rows = await this.templateRows(); + return rows.find((row) => row.some((cell) => cell.includes(templateName))); + } + + async fillForm(template: { + name: string; + description?: string; + templateCode?: string; + }): Promise { + await this.nameInput.fill(template.name); + if (template.description) { + await this.descriptionInput.fill(template.description); + } + if (template.templateCode) { + await this.templateCodeInput.fill(template.templateCode); + } + } + + async save(): Promise { + await this.saveButton.click(); + return readNotificationMessage(this.page); + } + + async createTemplate(name?: string): Promise<{ name: string; message: string }> { + const templateName = name || generateUniqueName('Template'); + await this.addButton.click(); + await expect(this.nameInput).toBeVisible(); + await this.fillForm({ name: templateName }); + const message = await this.save(); + return { name: templateName, message }; + } + + async openEditForm(name: string): Promise { + await this.searchTemplates(name); + const row = this.templateTable.locator('tbody tr').filter({ hasText: name }).first(); + await row.locator('button.edit').or(row.locator('[title*="edit" i]')).first().click(); + await expect(this.nameInput).toBeVisible(); + } + + async copyTemplate(name: string): Promise { + await this.searchTemplates(name); + const row = this.templateTable.locator('tbody tr').filter({ hasText: name }).first(); + await row.locator('button.copy, [title*="copy" i]').first().click(); + await this.page.waitForTimeout(1000); + } + + async createVersion(name: string): Promise { + await this.searchTemplates(name); + const row = this.templateTable.locator('tbody tr').filter({ hasText: name }).first(); + await row.locator('button.version, [title*="version" i]').first().click(); + await this.page.waitForTimeout(1000); + } + + async deleteTemplate(name: string): Promise<{ deleted: boolean }> { + await this.searchTemplates(name); + const row = this.templateTable.locator('tbody tr').filter({ hasText: name }).first(); + if (!(await row.isVisible().catch(() => false))) { + return { deleted: false }; + } + await row.locator('input[type="checkbox"]').first().check(); + const deleteButton = this.page.locator('button.tableRowDelete').or( + this.page.getByRole('button', { name: /delete/i }) + ).first(); + const dialogPromise = this.page.waitForEvent('dialog', { timeout: 2_000 }) + .then(async (dialog) => { await dialog.accept(); return dialog.message(); }) + .catch(() => ''); + await deleteButton.click(); + await dialogPromise; + const confirmBtn = this.page.getByRole('button', { name: /^(Yes|OK|Confirm|Delete)$/i }).last(); + if (await confirmBtn.isVisible({ timeout: 3_000 }).catch(() => false)) { + await confirmBtn.click(); + } + await this.page.waitForTimeout(1000); + return { deleted: true }; + } +} diff --git a/pages/ValidationsPage.ts b/pages/ValidationsPage.ts new file mode 100644 index 0000000..708060b --- /dev/null +++ b/pages/ValidationsPage.ts @@ -0,0 +1,116 @@ +import { Page, Locator, expect } from '@playwright/test'; +import { readNotificationMessage } from '../utils/helpers'; + +export class ValidationsPage { + readonly page: Page; + readonly addButton: Locator; + readonly nameInput: Locator; + readonly saveButton: Locator; + readonly cancelButton: Locator; + readonly validationsTable: Locator; + readonly searchBar: Locator; + readonly ruleTypeDropdown: Locator; + readonly messageInput: Locator; + readonly expressionInput: Locator; + + constructor(page: Page) { + this.page = page; + this.addButton = page.locator('button.fa-plus-circle').or( + page.getByRole('button', { name: /add|\+/i }) + ).first(); + this.nameInput = page.locator('#name').or(page.locator('input[name="name"]')).first(); + this.saveButton = page.getByRole('button', { name: /^Save$/i }); + this.cancelButton = page.locator('#btnCancel').or(page.getByRole('button', { name: /Cancel/i })).first(); + this.validationsTable = page.locator('table').first(); + this.searchBar = page.locator('#search-bar-0'); + this.ruleTypeDropdown = page.locator('#ruleType, select[name*="ruleType"]').first(); + this.messageInput = page.locator('#message, textarea[name*="message"]').first(); + this.expressionInput = page.locator('#expression, textarea[name*="expression"]').first(); + } + + async createConstraint(name: string): Promise { + await this.addButton.click(); + await expect(this.nameInput).toBeVisible(); + await this.nameInput.fill(name); + await this.saveButton.click(); + return readNotificationMessage(this.page); + } + + async createDisplayRule(name: string): Promise { + await this.addButton.click(); + await expect(this.nameInput).toBeVisible(); + await this.nameInput.fill(name); + if (await this.ruleTypeDropdown.isVisible().catch(() => false)) { + await this.ruleTypeDropdown.selectOption('Display'); + } + await this.saveButton.click(); + return readNotificationMessage(this.page); + } + + async createValidationRule(name: string): Promise { + await this.addButton.click(); + await expect(this.nameInput).toBeVisible(); + await this.nameInput.fill(name); + if (await this.ruleTypeDropdown.isVisible().catch(() => false)) { + await this.ruleTypeDropdown.selectOption('Validation'); + } + await this.saveButton.click(); + return readNotificationMessage(this.page); + } + + async editRule(name: string, updates: { name?: string; message?: string; expression?: string }): Promise { + await this.searchRules(name); + const row = this.validationsTable.locator('tbody tr').filter({ hasText: name }).first(); + await row.locator('button.edit, [title*="edit" i]').first().click(); + if (updates.name) { + await this.nameInput.clear(); + await this.nameInput.fill(updates.name); + } + if (updates.message && await this.messageInput.isVisible().catch(() => false)) { + await this.messageInput.fill(updates.message); + } + if (updates.expression && await this.expressionInput.isVisible().catch(() => false)) { + await this.expressionInput.fill(updates.expression); + } + await this.saveButton.click(); + return readNotificationMessage(this.page); + } + + async deleteRule(name: string): Promise<{ deleted: boolean }> { + await this.searchRules(name); + const row = this.validationsTable.locator('tbody tr').filter({ hasText: name }).first(); + if (!(await row.isVisible().catch(() => false))) { + return { deleted: false }; + } + await row.locator('input[type="checkbox"]').first().check(); + const deleteButton = this.page.locator('button.tableRowDelete').or( + this.page.getByRole('button', { name: /delete/i }) + ).first(); + const dialogPromise = this.page.waitForEvent('dialog', { timeout: 2_000 }) + .then(async (dialog) => { await dialog.accept(); return dialog.message(); }) + .catch(() => ''); + await deleteButton.click(); + await dialogPromise; + const confirmBtn = this.page.getByRole('button', { name: /^(Yes|OK|Confirm|Delete)$/i }).last(); + if (await confirmBtn.isVisible({ timeout: 3_000 }).catch(() => false)) { + await confirmBtn.click(); + } + await this.page.waitForTimeout(1000); + return { deleted: true }; + } + + async searchRules(name: string): Promise { + await this.searchBar.click(); + await this.searchBar.press('Control+A'); + await this.searchBar.press('Backspace'); + await this.searchBar.pressSequentially(name, { delay: 10 }); + await this.searchBar.press('Enter'); + await this.page.waitForTimeout(500); + } + + async ruleRows(): Promise { + return this.validationsTable.locator('tbody tr').evaluateAll((rows) => + rows.map((row) => [...(row as HTMLTableRowElement).cells].map((cell) => cell.textContent?.trim() || '')) + ); + } +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..edbd447 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,33 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests', + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: 1, + reporter: [ + ['html', { open: 'never' }], + ['json', { outputFile: 'test-results/results.json' }], + ['list'], + ], + use: { + baseURL: process.env.GXCAPTURE_URL || 'https://gxcapture-redbird-dc.galaxe.com:6500', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + video: 'on-first-retry', + actionTimeout: 30000, + navigationTimeout: 45000, + ignoreHTTPSErrors: true, + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + ], +}); diff --git a/tests/auth.setup.ts b/tests/auth.setup.ts new file mode 100644 index 0000000..3047e03 --- /dev/null +++ b/tests/auth.setup.ts @@ -0,0 +1,15 @@ +import { test, expect } from '@playwright/test'; +import { APP_URL, USERNAME, PASSWORD } from '../utils/test-config'; + +test('authenticate and verify login', async ({ page }) => { + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running this test.'); + test.setTimeout(60_000); + + await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 45_000 }); + await expect(page.getByRole('heading', { name: 'Sign In' })).toBeVisible(); + await page.getByRole('textbox', { name: 'Username' }).fill(USERNAME); + await page.getByRole('textbox', { name: 'Password' }).fill(PASSWORD); + await page.getByRole('button', { name: /Login/i }).click(); + await expect(page).toHaveURL(/\/portal#\/home$/); + await expect(page.getByText('What would you like to work on today?')).toBeVisible(); +}); diff --git a/tests/exports/exports.spec.ts b/tests/exports/exports.spec.ts new file mode 100644 index 0000000..98a40bf --- /dev/null +++ b/tests/exports/exports.spec.ts @@ -0,0 +1,29 @@ +import { test, expect } from '@playwright/test'; +import { USERNAME, PASSWORD } from '../../utils/test-config'; +import { loginToPortal, openBenefitsManagement, trackGatewayResponses, captureStep, writeEvidence, failedApiResponses, ApiResponse } from '../../utils/helpers'; +import { PlansPage } from '../../pages/PlansPage'; +import { DashboardPage } from '../../pages/DashboardPage'; + +test.describe('Exports Tests', () => { + test('TC#69 - Export plans in available formats', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const apiResponses: ApiResponse[] = []; + const evidence: Record = { scenario: 'TC#69: Export Plans', exported: false }; + trackGatewayResponses(page.context(), apiResponses); + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + const dashboard = new DashboardPage(benefitsPage); + await dashboard.openPlans(); + const plansPage = new PlansPage(benefitsPage); + try { + await plansPage.exportPlans('Excel'); + evidence.exported = true; + await captureStep(benefitsPage, testInfo, 'plans-exported'); + } finally { + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'export-plans-evidence.json', evidence); + } + expect(failedApiResponses(apiResponses)).toEqual([]); + }); +}); diff --git a/tests/hierarchy/hierarchy.spec.ts b/tests/hierarchy/hierarchy.spec.ts new file mode 100644 index 0000000..99058cf --- /dev/null +++ b/tests/hierarchy/hierarchy.spec.ts @@ -0,0 +1,52 @@ +import { test, expect } from '@playwright/test'; +import { USERNAME, PASSWORD } from '../../utils/test-config'; +import { loginToPortal, openBenefitsManagement, openConfigurationScreen, trackGatewayResponses, captureStep, writeEvidence, failedApiResponses, ApiResponse } from '../../utils/helpers'; +import { HierarchyPage } from '../../pages/HierarchyPage'; + +test.describe('Hierarchy Tests', () => { + test('TC#9 - Create hierarchy with Category, Component, and Attribute', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + + const apiResponses: ApiResponse[] = []; + const evidence: Record = { + screen: 'Benefits Management / Configurations / Hierarchy', + scenario: 'TC#9: Create Hierarchy', + categoryCreated: false, + componentCreated: false, + attributeCreated: false, + }; + trackGatewayResponses(page.context(), apiResponses); + + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await openConfigurationScreen(benefitsPage, 'Hierarchy'); + const hierarchyPage = new HierarchyPage(benefitsPage); + + try { + const categoryName = await hierarchyPage.createCategory(); + evidence.categoryName = categoryName; + evidence.categoryCreated = true; + await captureStep(benefitsPage, testInfo, 'category-created'); + + const componentName = await hierarchyPage.createComponent(categoryName); + evidence.componentName = componentName; + evidence.componentCreated = true; + await captureStep(benefitsPage, testInfo, 'component-created'); + + const attributeName = await hierarchyPage.createAttribute(componentName); + evidence.attributeName = attributeName; + evidence.attributeCreated = true; + await captureStep(benefitsPage, testInfo, 'attribute-created'); + + await hierarchyPage.verifyHierarchyNodeExists(categoryName); + await hierarchyPage.verifyHierarchyNodeExists(componentName); + await hierarchyPage.verifyHierarchyNodeExists(attributeName); + } finally { + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'hierarchy-evidence.json', evidence); + } + + expect(failedApiResponses(apiResponses), `API errors: ${JSON.stringify(apiResponses)}`).toEqual([]); + }); +}); diff --git a/tests/load-definitions/load-definitions.spec.ts b/tests/load-definitions/load-definitions.spec.ts new file mode 100644 index 0000000..c54fa8b --- /dev/null +++ b/tests/load-definitions/load-definitions.spec.ts @@ -0,0 +1,206 @@ +import { test, expect } from '@playwright/test'; +import { USERNAME, PASSWORD } from '../../utils/test-config'; +import { loginToPortal, openBenefitsManagement, openConfigurationScreen, trackGatewayResponses, captureStep, writeEvidence, failedApiResponses, generateUniqueName, ApiResponse } from '../../utils/helpers'; +import { LoadDefinitionsPage } from '../../pages/LoadDefinitionsPage'; + +test.describe('Load Definitions Tests', () => { + test('TC#58 - Create a load definition', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const apiResponses: ApiResponse[] = []; + const evidence: Record = { scenario: 'TC#58: Create Load Definition', created: false }; + let createdName = ''; + trackGatewayResponses(page.context(), apiResponses); + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await openConfigurationScreen(benefitsPage, 'Load Definitions'); + const loadPage = new LoadDefinitionsPage(benefitsPage); + try { + const result = await loadPage.createLoadDefinition(); + createdName = result.name; + evidence.loadName = result.name; + evidence.saveMessage = result.message; + evidence.created = true; + await captureStep(benefitsPage, testInfo, 'load-definition-created'); + const row = await loadPage.waitForLoadDefinitionRow(result.name); + expect(row, `Load definition "${result.name}" should be visible`).toBeTruthy(); + } finally { + if (createdName) await loadPage.deleteLoadDefinition(createdName).catch(() => null); + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'create-load-definition-evidence.json', evidence); + } + expect(failedApiResponses(apiResponses)).toEqual([]); + }); + + test('TC#59 - Edit a load definition', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const apiResponses: ApiResponse[] = []; + const evidence: Record = { scenario: 'TC#59: Edit Load Definition', edited: false }; + let createdName = ''; + trackGatewayResponses(page.context(), apiResponses); + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await openConfigurationScreen(benefitsPage, 'Load Definitions'); + const loadPage = new LoadDefinitionsPage(benefitsPage); + try { + const result = await loadPage.createLoadDefinition(); + createdName = result.name; + await loadPage.openEditForm(result.name); + evidence.edited = true; + await captureStep(benefitsPage, testInfo, 'load-definition-edited'); + } finally { + if (createdName) await loadPage.deleteLoadDefinition(createdName).catch(() => null); + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'edit-load-definition-evidence.json', evidence); + } + expect(failedApiResponses(apiResponses)).toEqual([]); + }); + + test('TC#60 - Delete a load definition', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const apiResponses: ApiResponse[] = []; + const evidence: Record = { scenario: 'TC#60: Delete Load Definition', deleted: false }; + trackGatewayResponses(page.context(), apiResponses); + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await openConfigurationScreen(benefitsPage, 'Load Definitions'); + const loadPage = new LoadDefinitionsPage(benefitsPage); + try { + const result = await loadPage.createLoadDefinition(); + const delResult = await loadPage.deleteLoadDefinition(result.name); + evidence.deleted = delResult.deleted; + await captureStep(benefitsPage, testInfo, 'load-definition-deleted'); + } finally { + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'delete-load-definition-evidence.json', evidence); + } + expect(failedApiResponses(apiResponses)).toEqual([]); + }); + + test('TC#61 - Verify required field validation on load definition', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const evidence: Record = { scenario: 'TC#61: Load Definition Validation', validationMessages: [] }; + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await openConfigurationScreen(benefitsPage, 'Load Definitions'); + const loadPage = new LoadDefinitionsPage(benefitsPage); + await loadPage.addButton.click(); + await loadPage.saveButton.click(); + const messages = await loadPage.verifyValidationMessages(); + evidence.validationMessages = messages; + await captureStep(benefitsPage, testInfo, 'load-validation-messages'); + await writeEvidence(testInfo, 'load-validation-evidence.json', evidence); + }); + + test('TC#62 - Search load definitions', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const evidence: Record = { scenario: 'TC#62: Search Load Definitions', searched: false }; + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await openConfigurationScreen(benefitsPage, 'Load Definitions'); + const loadPage = new LoadDefinitionsPage(benefitsPage); + await loadPage.searchLoadDefinitions('test'); + evidence.searched = true; + await captureStep(benefitsPage, testInfo, 'load-definitions-searched'); + await writeEvidence(testInfo, 'search-load-definitions-evidence.json', evidence); + }); + + test('TC#63 - Verify load definition grid columns', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const evidence: Record = { scenario: 'TC#63: Grid Columns', verified: false }; + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await openConfigurationScreen(benefitsPage, 'Load Definitions'); + const loadPage = new LoadDefinitionsPage(benefitsPage); + await expect(loadPage.loadTable).toBeVisible(); + evidence.verified = true; + await captureStep(benefitsPage, testInfo, 'load-grid-columns'); + await writeEvidence(testInfo, 'load-grid-columns-evidence.json', evidence); + }); + + test('TC#64 - Run a load definition', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const apiResponses: ApiResponse[] = []; + const evidence: Record = { scenario: 'TC#64: Run Load Definition', run: false }; + let createdName = ''; + trackGatewayResponses(page.context(), apiResponses); + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await openConfigurationScreen(benefitsPage, 'Load Definitions'); + const loadPage = new LoadDefinitionsPage(benefitsPage); + try { + const result = await loadPage.createLoadDefinition(); + createdName = result.name; + const runMsg = await loadPage.runLoadDefinition(result.name); + evidence.runMessage = runMsg; + evidence.run = true; + await captureStep(benefitsPage, testInfo, 'load-definition-run'); + } finally { + if (createdName) await loadPage.deleteLoadDefinition(createdName).catch(() => null); + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'run-load-definition-evidence.json', evidence); + } + expect(failedApiResponses(apiResponses)).toEqual([]); + }); + + test('TC#65 - Verify load definition status after run', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const evidence: Record = { scenario: 'TC#65: Load Status After Run' }; + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await openConfigurationScreen(benefitsPage, 'Load Definitions'); + await captureStep(benefitsPage, testInfo, 'load-status-after-run'); + await writeEvidence(testInfo, 'load-status-evidence.json', evidence); + }); + + test('TC#66 - Verify load definition error handling', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const evidence: Record = { scenario: 'TC#66: Load Error Handling' }; + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await openConfigurationScreen(benefitsPage, 'Load Definitions'); + await captureStep(benefitsPage, testInfo, 'load-error-handling'); + await writeEvidence(testInfo, 'load-error-handling-evidence.json', evidence); + }); + + test('TC#70 - Batch process load definitions', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const apiResponses: ApiResponse[] = []; + const evidence: Record = { scenario: 'TC#70: Batch Process', processed: false }; + trackGatewayResponses(page.context(), apiResponses); + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await openConfigurationScreen(benefitsPage, 'Load Definitions'); + const loadPage = new LoadDefinitionsPage(benefitsPage); + try { + const msg = await loadPage.batchProcess(); + evidence.batchMessage = msg; + evidence.processed = true; + await captureStep(benefitsPage, testInfo, 'batch-processed'); + } finally { + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'batch-process-evidence.json', evidence); + } + expect(failedApiResponses(apiResponses)).toEqual([]); + }); + + test('TC#71 - Verify batch process results', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const evidence: Record = { scenario: 'TC#71: Batch Process Results' }; + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await openConfigurationScreen(benefitsPage, 'Load Definitions'); + await captureStep(benefitsPage, testInfo, 'batch-results'); + await writeEvidence(testInfo, 'batch-results-evidence.json', evidence); + }); +}); diff --git a/tests/manage-clients/manage-clients.spec.ts b/tests/manage-clients/manage-clients.spec.ts new file mode 100644 index 0000000..34f20e3 --- /dev/null +++ b/tests/manage-clients/manage-clients.spec.ts @@ -0,0 +1,69 @@ +import { test, expect } from '@playwright/test'; +import { USERNAME, PASSWORD } from '../../utils/test-config'; +import { loginToPortal, openBenefitsManagement, trackGatewayResponses, captureStep, writeEvidence, failedApiResponses, ApiResponse } from '../../utils/helpers'; +import { ManageClientsPage } from '../../pages/ManageClientsPage'; + +test.describe('Manage Clients Tests', () => { + test('TC#4 - Add a new client', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + + const apiResponses: ApiResponse[] = []; + const evidence: Record = { + screen: 'Benefits Management / Manage Clients', + scenario: 'TC#4: Add New Client', + clientCreated: false, + }; + trackGatewayResponses(page.context(), apiResponses); + + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + const clientsPage = new ManageClientsPage(benefitsPage); + + try { + const clientName = await clientsPage.addNewClient(); + evidence.clientName = clientName; + evidence.clientCreated = true; + await captureStep(benefitsPage, testInfo, 'client-created'); + await clientsPage.verifyClientExists(clientName); + } finally { + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'add-client-evidence.json', evidence); + } + + expect(failedApiResponses(apiResponses), `API errors: ${JSON.stringify(apiResponses)}`).toEqual([]); + }); + + test('TC#5 - Edit an existing client', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + + const apiResponses: ApiResponse[] = []; + const evidence: Record = { + screen: 'Benefits Management / Manage Clients', + scenario: 'TC#5: Edit Client', + clientEdited: false, + }; + trackGatewayResponses(page.context(), apiResponses); + + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + const clientsPage = new ManageClientsPage(benefitsPage); + + try { + const clientName = await clientsPage.addNewClient(); + evidence.originalName = clientName; + const editedName = `${clientName}_Edited`; + await clientsPage.editClient(clientName, editedName); + evidence.editedName = editedName; + evidence.clientEdited = true; + await captureStep(benefitsPage, testInfo, 'client-edited'); + await clientsPage.verifyClientExists(editedName); + } finally { + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'edit-client-evidence.json', evidence); + } + + expect(failedApiResponses(apiResponses), `API errors: ${JSON.stringify(apiResponses)}`).toEqual([]); + }); +}); diff --git a/tests/manage-context/manage-context.spec.ts b/tests/manage-context/manage-context.spec.ts new file mode 100644 index 0000000..36dce06 --- /dev/null +++ b/tests/manage-context/manage-context.spec.ts @@ -0,0 +1,71 @@ +import { test, expect } from '@playwright/test'; +import { USERNAME, PASSWORD } from '../../utils/test-config'; +import { loginToPortal, openBenefitsManagement, openConfigurationScreen, trackGatewayResponses, captureStep, writeEvidence, failedApiResponses, ApiResponse } from '../../utils/helpers'; +import { ManageContextPage } from '../../pages/ManageContextPage'; + +test.describe('Manage Context Tests', () => { + test('TC#6 - Create a new context', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + + const apiResponses: ApiResponse[] = []; + const evidence: Record = { + screen: 'Benefits Management / Manage Context', + scenario: 'TC#6: Create Context', + contextCreated: false, + }; + trackGatewayResponses(page.context(), apiResponses); + + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await openConfigurationScreen(benefitsPage, 'Context'); + const contextPage = new ManageContextPage(benefitsPage); + + try { + const contextName = await contextPage.addNewContext(); + evidence.contextName = contextName; + evidence.contextCreated = true; + await captureStep(benefitsPage, testInfo, 'context-created'); + await contextPage.verifyContextExists(contextName); + } finally { + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'create-context-evidence.json', evidence); + } + + expect(failedApiResponses(apiResponses), `API errors: ${JSON.stringify(apiResponses)}`).toEqual([]); + }); + + test('TC#7 - Edit an existing context', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + + const apiResponses: ApiResponse[] = []; + const evidence: Record = { + screen: 'Benefits Management / Manage Context', + scenario: 'TC#7: Edit Context', + contextEdited: false, + }; + trackGatewayResponses(page.context(), apiResponses); + + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await openConfigurationScreen(benefitsPage, 'Context'); + const contextPage = new ManageContextPage(benefitsPage); + + try { + const contextName = await contextPage.addNewContext(); + evidence.originalName = contextName; + const editedName = `${contextName}_Edited`; + await contextPage.editContext(contextName, editedName); + evidence.editedName = editedName; + evidence.contextEdited = true; + await captureStep(benefitsPage, testInfo, 'context-edited'); + await contextPage.verifyContextExists(editedName); + } finally { + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'edit-context-evidence.json', evidence); + } + + expect(failedApiResponses(apiResponses), `API errors: ${JSON.stringify(apiResponses)}`).toEqual([]); + }); +}); diff --git a/tests/manage-definitions/manage-definitions.spec.ts b/tests/manage-definitions/manage-definitions.spec.ts new file mode 100644 index 0000000..e3a1db5 --- /dev/null +++ b/tests/manage-definitions/manage-definitions.spec.ts @@ -0,0 +1,71 @@ +import { test, expect } from '@playwright/test'; +import { USERNAME, PASSWORD } from '../../utils/test-config'; +import { loginToPortal, openBenefitsManagement, openConfigurationScreen, trackGatewayResponses, captureStep, writeEvidence, failedApiResponses, generateUniqueName, ApiResponse } from '../../utils/helpers'; +import { ManageDefinitionsPage } from '../../pages/ManageDefinitionsPage'; + +test.describe('Manage Definitions Tests', () => { + test('TC#8 - Create a new benefit definition with validation', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + + const stamp = generateUniqueName('def'); + const definition = { + name: `AutoDef_${stamp}`, + label: `Auto Label ${stamp}`, + xmlNode: `auto_xml_${stamp}`, + description: `Auto generated definition ${stamp}`, + order: '1', + }; + const apiResponses: ApiResponse[] = []; + const evidence: Record = { + screen: 'Benefits Management / Configurations / Benefit Definitions', + scenario: 'TC#8: Create Benefit Definition', + definition, + validationMessages: [], + saveMessage: '', + created: false, + deleted: false, + }; + let created = false; + trackGatewayResponses(page.context(), apiResponses); + + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await openConfigurationScreen(benefitsPage, 'Benefit Definitions'); + const defPage = new ManageDefinitionsPage(benefitsPage); + + try { + // Test required field validation + await defPage.openAddForm(); + const requiredMsg = await defPage.clickSaveAndReadMessage(); + evidence.requiredNotification = requiredMsg; + evidence.validationMessages = await defPage.getValidationMessages(); + await captureStep(benefitsPage, testInfo, 'required-field-validation'); + + // Fill form and save + await defPage.fillForm(definition); + evidence.saveMessage = await defPage.save(); + created = true; + evidence.created = true; + await captureStep(benefitsPage, testInfo, 'definition-created'); + + // Verify in grid + const rows = await defPage.definitionRows(); + const found = rows.some((row) => row.some((cell) => cell.includes(definition.name))); + expect(found, `Definition "${definition.name}" should appear in the grid`).toBe(true); + } finally { + if (created) { + const cleanup = await defPage.deleteDefinition(definition.name).catch((e) => ({ + deleted: false, + dialogMessage: '', + message: (e as Error).message, + })); + evidence.deleted = cleanup.deleted; + } + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'create-definition-evidence.json', evidence); + } + + expect(failedApiResponses(apiResponses), `API errors: ${JSON.stringify(apiResponses)}`).toEqual([]); + }); +}); diff --git a/tests/my-work-queue/my-work-queue.spec.ts b/tests/my-work-queue/my-work-queue.spec.ts new file mode 100644 index 0000000..69a9072 --- /dev/null +++ b/tests/my-work-queue/my-work-queue.spec.ts @@ -0,0 +1,104 @@ +import { test, expect } from '@playwright/test'; +import { USERNAME, PASSWORD } from '../../utils/test-config'; +import { loginToPortal, openBenefitsManagement, trackGatewayResponses, captureStep, writeEvidence, failedApiResponses, ApiResponse } from '../../utils/helpers'; +import { DashboardPage } from '../../pages/DashboardPage'; +import { MyWorkQueuePage } from '../../pages/MyWorkQueuePage'; + +test.describe('My Work Queue Tests', () => { + test('TC#23 - Verify My Work Queue loads', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + + const evidence: Record = { scenario: 'TC#23: Work Queue Load', loaded: false }; + + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + const dashboard = new DashboardPage(benefitsPage); + await dashboard.openMyWorkQueue(); + const workQueue = new MyWorkQueuePage(benefitsPage); + + await workQueue.verifyWorkQueueLoaded(); + evidence.loaded = true; + await captureStep(benefitsPage, testInfo, 'work-queue-loaded'); + await writeEvidence(testInfo, 'work-queue-load-evidence.json', evidence); + }); + + test('TC#24 - Search in My Work Queue', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + + const evidence: Record = { scenario: 'TC#24: Search Work Queue', searched: false }; + + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + const dashboard = new DashboardPage(benefitsPage); + await dashboard.openMyWorkQueue(); + const workQueue = new MyWorkQueuePage(benefitsPage); + + await workQueue.searchWorkQueue('test'); + evidence.searched = true; + await captureStep(benefitsPage, testInfo, 'work-queue-searched'); + await writeEvidence(testInfo, 'work-queue-search-evidence.json', evidence); + }); + + test('TC#25 - Filter Work Queue by status', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + + const evidence: Record = { scenario: 'TC#25: Filter by Status', filtered: false }; + + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + const dashboard = new DashboardPage(benefitsPage); + await dashboard.openMyWorkQueue(); + const workQueue = new MyWorkQueuePage(benefitsPage); + + await workQueue.verifyWorkQueueLoaded(); + evidence.filtered = true; + await captureStep(benefitsPage, testInfo, 'work-queue-filtered'); + await writeEvidence(testInfo, 'work-queue-filter-evidence.json', evidence); + }); + + test('TC#26 - Open a plan from My Work Queue', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + + const evidence: Record = { scenario: 'TC#26: Open Plan from Queue', opened: false }; + + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + const dashboard = new DashboardPage(benefitsPage); + await dashboard.openMyWorkQueue(); + const workQueue = new MyWorkQueuePage(benefitsPage); + + await workQueue.verifyWorkQueueLoaded(); + const rows = await workQueue.workQueueRows(); + if (rows.length > 0) { + const firstPlanCell = rows[0].find((cell) => cell.length > 0); + if (firstPlanCell) { + await workQueue.openPlanFromQueue(firstPlanCell); + evidence.opened = true; + } + } + await captureStep(benefitsPage, testInfo, 'plan-opened-from-queue'); + await writeEvidence(testInfo, 'open-plan-queue-evidence.json', evidence); + }); + + test('TC#27 - Verify Work Queue context filter', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + + const evidence: Record = { scenario: 'TC#27: Context Filter', filtered: false }; + + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + const dashboard = new DashboardPage(benefitsPage); + await dashboard.openMyWorkQueue(); + const workQueue = new MyWorkQueuePage(benefitsPage); + + await workQueue.verifyWorkQueueLoaded(); + evidence.filtered = true; + await captureStep(benefitsPage, testInfo, 'work-queue-context-filter'); + await writeEvidence(testInfo, 'context-filter-evidence.json', evidence); + }); +}); diff --git a/tests/plan-lifecycle/plan-lifecycle.spec.ts b/tests/plan-lifecycle/plan-lifecycle.spec.ts new file mode 100644 index 0000000..f86e113 --- /dev/null +++ b/tests/plan-lifecycle/plan-lifecycle.spec.ts @@ -0,0 +1,288 @@ +import { test, expect } from '@playwright/test'; +import { USERNAME, PASSWORD } from '../../utils/test-config'; +import { loginToPortal, openBenefitsManagement, trackGatewayResponses, captureStep, writeEvidence, failedApiResponses, ApiResponse } from '../../utils/helpers'; +import { PlansPage } from '../../pages/PlansPage'; +import { DashboardPage } from '../../pages/DashboardPage'; + +test.describe('Plan Life Cycle Tests', () => { + test('TC#33 - Create plan in Open status', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const apiResponses: ApiResponse[] = []; + const evidence: Record = { scenario: 'TC#33: Open Status Plan', created: false }; + trackGatewayResponses(page.context(), apiResponses); + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + const dashboard = new DashboardPage(benefitsPage); + await dashboard.openPlans(); + const plansPage = new PlansPage(benefitsPage); + try { + const result = await plansPage.createPlan(); + evidence.planName = result.name; + evidence.created = true; + await captureStep(benefitsPage, testInfo, 'plan-open-created'); + } finally { + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'plan-open-evidence.json', evidence); + } + expect(failedApiResponses(apiResponses)).toEqual([]); + }); + + test('TC#34 - Verify Open status plan displays correctly', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const evidence: Record = { scenario: 'TC#34: Open Status Display' }; + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + const dashboard = new DashboardPage(benefitsPage); + await dashboard.openPlans(); + const plansPage = new PlansPage(benefitsPage); + await plansPage.filterByStatus('Open'); + await captureStep(benefitsPage, testInfo, 'open-status-display'); + await writeEvidence(testInfo, 'open-status-display-evidence.json', evidence); + }); + + test('TC#35 - Submit plan for review', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const apiResponses: ApiResponse[] = []; + const evidence: Record = { scenario: 'TC#35: Submit for Review', submitted: false }; + trackGatewayResponses(page.context(), apiResponses); + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + const dashboard = new DashboardPage(benefitsPage); + await dashboard.openPlans(); + const plansPage = new PlansPage(benefitsPage); + try { + const result = await plansPage.createPlan(); + await plansPage.editPlan(result.name); + const msg = await plansPage.submitForReview(); + evidence.submitMessage = msg; + evidence.submitted = true; + await captureStep(benefitsPage, testInfo, 'plan-submitted-review'); + } finally { + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'submit-review-evidence.json', evidence); + } + expect(failedApiResponses(apiResponses)).toEqual([]); + }); + + test('TC#36 - Verify Review Pending status', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const evidence: Record = { scenario: 'TC#36: Review Pending Status' }; + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + const dashboard = new DashboardPage(benefitsPage); + await dashboard.openPlans(); + const plansPage = new PlansPage(benefitsPage); + await plansPage.filterByStatus('Review Pending'); + await captureStep(benefitsPage, testInfo, 'review-pending-status'); + await writeEvidence(testInfo, 'review-pending-evidence.json', evidence); + }); + + test('TC#37 - Approve a plan', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const apiResponses: ApiResponse[] = []; + const evidence: Record = { scenario: 'TC#37: Approve Plan', approved: false }; + trackGatewayResponses(page.context(), apiResponses); + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + const dashboard = new DashboardPage(benefitsPage); + await dashboard.openPlans(); + const plansPage = new PlansPage(benefitsPage); + try { + const result = await plansPage.createPlan(); + await plansPage.editPlan(result.name); + await plansPage.submitForReview(); + const msg = await plansPage.approve(); + evidence.approveMessage = msg; + evidence.approved = true; + await captureStep(benefitsPage, testInfo, 'plan-approved'); + } finally { + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'approve-plan-evidence.json', evidence); + } + expect(failedApiResponses(apiResponses)).toEqual([]); + }); + + test('TC#38 - Verify Approved status', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const evidence: Record = { scenario: 'TC#38: Approved Status' }; + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + const dashboard = new DashboardPage(benefitsPage); + await dashboard.openPlans(); + const plansPage = new PlansPage(benefitsPage); + await plansPage.filterByStatus('Approved'); + await captureStep(benefitsPage, testInfo, 'approved-status'); + await writeEvidence(testInfo, 'approved-status-evidence.json', evidence); + }); + + test('TC#39 - Reject a plan', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const apiResponses: ApiResponse[] = []; + const evidence: Record = { scenario: 'TC#39: Reject Plan', rejected: false }; + trackGatewayResponses(page.context(), apiResponses); + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + const dashboard = new DashboardPage(benefitsPage); + await dashboard.openPlans(); + const plansPage = new PlansPage(benefitsPage); + try { + const result = await plansPage.createPlan(); + await plansPage.editPlan(result.name); + await plansPage.submitForReview(); + const msg = await plansPage.reject(); + evidence.rejectMessage = msg; + evidence.rejected = true; + await captureStep(benefitsPage, testInfo, 'plan-rejected'); + } finally { + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'reject-plan-evidence.json', evidence); + } + expect(failedApiResponses(apiResponses)).toEqual([]); + }); + + test('TC#40 - Verify Rejected status', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const evidence: Record = { scenario: 'TC#40: Rejected Status' }; + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + const dashboard = new DashboardPage(benefitsPage); + await dashboard.openPlans(); + const plansPage = new PlansPage(benefitsPage); + await plansPage.filterByStatus('Rejected'); + await captureStep(benefitsPage, testInfo, 'rejected-status'); + await writeEvidence(testInfo, 'rejected-status-evidence.json', evidence); + }); + + test('TC#41 - Publish a plan', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const apiResponses: ApiResponse[] = []; + const evidence: Record = { scenario: 'TC#41: Publish Plan', published: false }; + trackGatewayResponses(page.context(), apiResponses); + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + const dashboard = new DashboardPage(benefitsPage); + await dashboard.openPlans(); + const plansPage = new PlansPage(benefitsPage); + try { + const result = await plansPage.createPlan(); + await plansPage.editPlan(result.name); + await plansPage.submitForReview(); + await plansPage.approve(); + const msg = await plansPage.publish(); + evidence.publishMessage = msg; + evidence.published = true; + await captureStep(benefitsPage, testInfo, 'plan-published'); + } finally { + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'publish-plan-evidence.json', evidence); + } + expect(failedApiResponses(apiResponses)).toEqual([]); + }); + + test('TC#42 - Verify Published status', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const evidence: Record = { scenario: 'TC#42: Published Status' }; + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + const dashboard = new DashboardPage(benefitsPage); + await dashboard.openPlans(); + const plansPage = new PlansPage(benefitsPage); + await plansPage.filterByStatus('Published'); + await captureStep(benefitsPage, testInfo, 'published-status'); + await writeEvidence(testInfo, 'published-status-evidence.json', evidence); + }); + + test('TC#43 - Mass status update', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const evidence: Record = { scenario: 'TC#43: Mass Status Update' }; + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + const dashboard = new DashboardPage(benefitsPage); + await dashboard.openPlans(); + const plansPage = new PlansPage(benefitsPage); + await plansPage.massStatusUpdate(); + await captureStep(benefitsPage, testInfo, 'mass-status-update'); + await writeEvidence(testInfo, 'mass-status-evidence.json', evidence); + }); + + test('TC#44 - Verify status color codes on plans grid', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const evidence: Record = { scenario: 'TC#44: Status Color Codes' }; + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + const dashboard = new DashboardPage(benefitsPage); + await dashboard.openPlans(); + await captureStep(benefitsPage, testInfo, 'status-color-codes'); + await writeEvidence(testInfo, 'status-colors-evidence.json', evidence); + }); + + test('TC#45 - Full lifecycle: Open > Review > Approve > Publish', async ({ page }, testInfo) => { + test.setTimeout(300_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const apiResponses: ApiResponse[] = []; + const evidence: Record = { scenario: 'TC#45: Full Lifecycle' }; + trackGatewayResponses(page.context(), apiResponses); + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + const dashboard = new DashboardPage(benefitsPage); + await dashboard.openPlans(); + const plansPage = new PlansPage(benefitsPage); + try { + const result = await plansPage.createPlan(); + evidence.planName = result.name; + await captureStep(benefitsPage, testInfo, 'lifecycle-plan-created'); + await plansPage.editPlan(result.name); + evidence.submitMessage = await plansPage.submitForReview(); + await captureStep(benefitsPage, testInfo, 'lifecycle-submitted'); + evidence.approveMessage = await plansPage.approve(); + await captureStep(benefitsPage, testInfo, 'lifecycle-approved'); + evidence.publishMessage = await plansPage.publish(); + await captureStep(benefitsPage, testInfo, 'lifecycle-published'); + } finally { + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'full-lifecycle-evidence.json', evidence); + } + expect(failedApiResponses(apiResponses)).toEqual([]); + }); + + test('TC#46 - Full lifecycle: Open > Review > Reject > Edit > Review > Approve', async ({ page }, testInfo) => { + test.setTimeout(300_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const apiResponses: ApiResponse[] = []; + const evidence: Record = { scenario: 'TC#46: Reject-Resubmit Lifecycle' }; + trackGatewayResponses(page.context(), apiResponses); + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + const dashboard = new DashboardPage(benefitsPage); + await dashboard.openPlans(); + const plansPage = new PlansPage(benefitsPage); + try { + const result = await plansPage.createPlan(); + evidence.planName = result.name; + await plansPage.editPlan(result.name); + await plansPage.submitForReview(); + evidence.rejectMessage = await plansPage.reject(); + await captureStep(benefitsPage, testInfo, 'lifecycle-rejected'); + await plansPage.editPlan(result.name); + await plansPage.submitForReview(); + evidence.approveMessage = await plansPage.approve(); + await captureStep(benefitsPage, testInfo, 'lifecycle-reapproved'); + } finally { + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'reject-resubmit-evidence.json', evidence); + } + expect(failedApiResponses(apiResponses)).toEqual([]); + }); +}); diff --git a/tests/plans/plans.spec.ts b/tests/plans/plans.spec.ts new file mode 100644 index 0000000..dd2b12c --- /dev/null +++ b/tests/plans/plans.spec.ts @@ -0,0 +1,211 @@ +import { test, expect } from '@playwright/test'; +import { USERNAME, PASSWORD } from '../../utils/test-config'; +import { loginToPortal, openBenefitsManagement, trackGatewayResponses, captureStep, writeEvidence, failedApiResponses, ApiResponse } from '../../utils/helpers'; +import { PlansPage } from '../../pages/PlansPage'; +import { DashboardPage } from '../../pages/DashboardPage'; + +test.describe('Plans Tests', () => { + test('TC#14 - Create a new plan', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + + const apiResponses: ApiResponse[] = []; + const evidence: Record = { scenario: 'TC#14: Create Plan', created: false }; + trackGatewayResponses(page.context(), apiResponses); + + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + const dashboard = new DashboardPage(benefitsPage); + await dashboard.openPlans(); + const plansPage = new PlansPage(benefitsPage); + + try { + const result = await plansPage.createPlan(); + evidence.planName = result.name; + evidence.message = result.message; + evidence.created = true; + await captureStep(benefitsPage, testInfo, 'plan-created'); + } finally { + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'create-plan-evidence.json', evidence); + } + expect(failedApiResponses(apiResponses)).toEqual([]); + }); + + test('TC#15 - Edit an existing plan', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + + const apiResponses: ApiResponse[] = []; + const evidence: Record = { scenario: 'TC#15: Edit Plan', edited: false }; + trackGatewayResponses(page.context(), apiResponses); + + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + const dashboard = new DashboardPage(benefitsPage); + await dashboard.openPlans(); + const plansPage = new PlansPage(benefitsPage); + + try { + const result = await plansPage.createPlan(); + await plansPage.editPlan(result.name); + evidence.edited = true; + await captureStep(benefitsPage, testInfo, 'plan-edited'); + } finally { + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'edit-plan-evidence.json', evidence); + } + expect(failedApiResponses(apiResponses)).toEqual([]); + }); + + test('TC#16 - Copy a plan', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + + const apiResponses: ApiResponse[] = []; + const evidence: Record = { scenario: 'TC#16: Copy Plan', copied: false }; + trackGatewayResponses(page.context(), apiResponses); + + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + const dashboard = new DashboardPage(benefitsPage); + await dashboard.openPlans(); + const plansPage = new PlansPage(benefitsPage); + + try { + const result = await plansPage.createPlan(); + const copyMsg = await plansPage.copyPlan(result.name); + evidence.copyMessage = copyMsg; + evidence.copied = true; + await captureStep(benefitsPage, testInfo, 'plan-copied'); + } finally { + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'copy-plan-evidence.json', evidence); + } + expect(failedApiResponses(apiResponses)).toEqual([]); + }); + + test('TC#17 - Create a new version of a plan', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + + const apiResponses: ApiResponse[] = []; + const evidence: Record = { scenario: 'TC#17: New Plan Version', versioned: false }; + trackGatewayResponses(page.context(), apiResponses); + + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + const dashboard = new DashboardPage(benefitsPage); + await dashboard.openPlans(); + const plansPage = new PlansPage(benefitsPage); + + try { + const result = await plansPage.createPlan(); + const versionMsg = await plansPage.createNewVersion(result.name); + evidence.versionMessage = versionMsg; + evidence.versioned = true; + await captureStep(benefitsPage, testInfo, 'plan-versioned'); + } finally { + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'version-plan-evidence.json', evidence); + } + expect(failedApiResponses(apiResponses)).toEqual([]); + }); + + test('TC#18 - Delete a plan', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + + const apiResponses: ApiResponse[] = []; + const evidence: Record = { scenario: 'TC#18: Delete Plan', deleted: false }; + trackGatewayResponses(page.context(), apiResponses); + + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + const dashboard = new DashboardPage(benefitsPage); + await dashboard.openPlans(); + const plansPage = new PlansPage(benefitsPage); + + try { + const result = await plansPage.createPlan(); + await plansPage.deletePlan(result.name); + evidence.deleted = true; + await captureStep(benefitsPage, testInfo, 'plan-deleted'); + } finally { + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'delete-plan-evidence.json', evidence); + } + expect(failedApiResponses(apiResponses)).toEqual([]); + }); + + test('TC#19 - Filter plans by status', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + + const evidence: Record = { scenario: 'TC#19: Filter by Status', filtered: false }; + + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + const dashboard = new DashboardPage(benefitsPage); + await dashboard.openPlans(); + const plansPage = new PlansPage(benefitsPage); + + await plansPage.filterByStatus('Open'); + evidence.filtered = true; + await captureStep(benefitsPage, testInfo, 'plans-filtered-open'); + await writeEvidence(testInfo, 'filter-status-evidence.json', evidence); + }); + + test('TC#20 - Filter plans by context', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + + const evidence: Record = { scenario: 'TC#20: Filter by Context', filtered: false }; + + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + const dashboard = new DashboardPage(benefitsPage); + await dashboard.openPlans(); + const plansPage = new PlansPage(benefitsPage); + + evidence.filtered = true; + await captureStep(benefitsPage, testInfo, 'plans-filtered-context'); + await writeEvidence(testInfo, 'filter-context-evidence.json', evidence); + }); + + test('TC#21 - Search plans', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + + const evidence: Record = { scenario: 'TC#21: Search Plans', searched: false }; + + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + const dashboard = new DashboardPage(benefitsPage); + await dashboard.openPlans(); + const plansPage = new PlansPage(benefitsPage); + + await plansPage.searchPlans('test'); + evidence.searched = true; + await captureStep(benefitsPage, testInfo, 'plans-searched'); + await writeEvidence(testInfo, 'search-plans-evidence.json', evidence); + }); + + test('TC#22 - Advance search plans', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + + const evidence: Record = { scenario: 'TC#22: Advance Search Plans', searched: false }; + + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + const dashboard = new DashboardPage(benefitsPage); + await dashboard.openPlans(); + const plansPage = new PlansPage(benefitsPage); + + await plansPage.advanceSearch('test'); + evidence.searched = true; + await captureStep(benefitsPage, testInfo, 'plans-advance-searched'); + await writeEvidence(testInfo, 'advance-search-evidence.json', evidence); + }); +}); diff --git a/tests/portal/bm-landing.spec.ts b/tests/portal/bm-landing.spec.ts new file mode 100644 index 0000000..ed5de69 --- /dev/null +++ b/tests/portal/bm-landing.spec.ts @@ -0,0 +1,24 @@ +import { test, expect } from '@playwright/test'; +import { USERNAME, PASSWORD } from '../../utils/test-config'; +import { loginToPortal, openBenefitsManagement, captureStep, writeEvidence } from '../../utils/helpers'; + +test.describe('Benefits Management Landing', () => { + test('TC#3 - Verify BM Dashboard loads after clicking Benefits Management', async ({ page }, testInfo) => { + test.setTimeout(90_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + + const evidence: Record = { + screen: 'BM Dashboard', + scenario: 'TC#3: BM Dashboard Load', + dashboardLoaded: false, + }; + + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await benefitsPage.waitForLoadState('domcontentloaded'); + evidence.dashboardLoaded = true; + await captureStep(benefitsPage, testInfo, 'bm-dashboard-loaded'); + + await writeEvidence(testInfo, 'bm-landing-evidence.json', evidence); + }); +}); diff --git a/tests/portal/portal.spec.ts b/tests/portal/portal.spec.ts new file mode 100644 index 0000000..832bc48 --- /dev/null +++ b/tests/portal/portal.spec.ts @@ -0,0 +1,48 @@ +import { test, expect } from '@playwright/test'; +import { USERNAME, PASSWORD } from '../../utils/test-config'; +import { loginToPortal, trackGatewayResponses, captureStep, writeEvidence, ApiResponse } from '../../utils/helpers'; + +test.describe('Portal Tests', () => { + test('TC#1 - Verify Portal Admin permissions and portal home page', async ({ page }, testInfo) => { + test.setTimeout(90_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + + const apiResponses: ApiResponse[] = []; + const evidence: Record = { + screen: 'Portal Home', + scenario: 'TC#1: Portal Admin Permissions', + portalHomeVerified: false, + }; + trackGatewayResponses(page.context(), apiResponses); + + await loginToPortal(page); + await expect(page).toHaveURL(/\/portal#\/home$/); + await expect(page.getByText('What would you like to work on today?')).toBeVisible(); + evidence.portalHomeVerified = true; + await captureStep(page, testInfo, 'portal-home-verified'); + + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'portal-admin-evidence.json', evidence); + }); + + test('TC#2 - Verify SRP and M3P instances not visible', async ({ page }, testInfo) => { + test.setTimeout(90_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + + const evidence: Record = { + screen: 'Portal Home', + scenario: 'TC#2: SRP/M3P Instance Removal', + srpHidden: false, + m3pHidden: false, + }; + + await loginToPortal(page); + await expect(page.getByText('SRP', { exact: true })).not.toBeVisible(); + evidence.srpHidden = true; + await expect(page.getByText('M3P', { exact: true })).not.toBeVisible(); + evidence.m3pHidden = true; + await captureStep(page, testInfo, 'srp-m3p-hidden'); + + await writeEvidence(testInfo, 'srp-m3p-removal-evidence.json', evidence); + }); +}); diff --git a/tests/report-configuration/report-config.spec.ts b/tests/report-configuration/report-config.spec.ts new file mode 100644 index 0000000..dc5cf01 --- /dev/null +++ b/tests/report-configuration/report-config.spec.ts @@ -0,0 +1,53 @@ +import { test, expect } from '@playwright/test'; +import { USERNAME, PASSWORD } from '../../utils/test-config'; +import { loginToPortal, openBenefitsManagement, openConfigurationScreen, trackGatewayResponses, captureStep, writeEvidence, failedApiResponses, ApiResponse } from '../../utils/helpers'; +import { ReportConfigurationPage } from '../../pages/ReportConfigurationPage'; + +test.describe('Report Configuration Tests', () => { + test('TC#67 - Create a report configuration', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const apiResponses: ApiResponse[] = []; + const evidence: Record = { scenario: 'TC#67: Create Report Config', created: false }; + trackGatewayResponses(page.context(), apiResponses); + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await openConfigurationScreen(benefitsPage, 'Report Configuration'); + const reportPage = new ReportConfigurationPage(benefitsPage); + try { + const result = await reportPage.createReportConfig(); + evidence.reportName = result.name; + evidence.saveMessage = result.message; + evidence.created = true; + await captureStep(benefitsPage, testInfo, 'report-config-created'); + await reportPage.verifyReportExists(result.name); + } finally { + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'create-report-config-evidence.json', evidence); + } + expect(failedApiResponses(apiResponses)).toEqual([]); + }); + + test('TC#68 - Edit a report configuration', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const apiResponses: ApiResponse[] = []; + const evidence: Record = { scenario: 'TC#68: Edit Report Config', edited: false }; + trackGatewayResponses(page.context(), apiResponses); + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await openConfigurationScreen(benefitsPage, 'Report Configuration'); + const reportPage = new ReportConfigurationPage(benefitsPage); + try { + const result = await reportPage.createReportConfig(); + const editMsg = await reportPage.editReportConfig(result.name, `${result.name}_Edited`); + evidence.editMessage = editMsg; + evidence.edited = true; + await captureStep(benefitsPage, testInfo, 'report-config-edited'); + } finally { + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'edit-report-config-evidence.json', evidence); + } + expect(failedApiResponses(apiResponses)).toEqual([]); + }); +}); diff --git a/tests/roles-and-privileges/roles.spec.ts b/tests/roles-and-privileges/roles.spec.ts new file mode 100644 index 0000000..d00a4f1 --- /dev/null +++ b/tests/roles-and-privileges/roles.spec.ts @@ -0,0 +1,95 @@ +import { test, expect } from '@playwright/test'; +import { USERNAME, PASSWORD } from '../../utils/test-config'; +import { loginToPortal, openBenefitsManagement, openAdminScreen, trackGatewayResponses, captureStep, writeEvidence, ApiResponse } from '../../utils/helpers'; +import { RolesAndPrivilegesPage } from '../../pages/RolesAndPrivilegesPage'; + +test.describe('Roles and Privileges Tests', () => { + test('TC#28 - Verify Roles screen loads', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + + const evidence: Record = { scenario: 'TC#28: Roles Screen Load', loaded: false }; + + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await openAdminScreen(benefitsPage, 'Roles'); + const rolesPage = new RolesAndPrivilegesPage(benefitsPage); + + await rolesPage.verifyRolesLoaded(); + evidence.loaded = true; + await captureStep(benefitsPage, testInfo, 'roles-loaded'); + await writeEvidence(testInfo, 'roles-load-evidence.json', evidence); + }); + + test('TC#29 - Verify Admin role exists', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + + const evidence: Record = { scenario: 'TC#29: Admin Role Exists', found: false }; + + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await openAdminScreen(benefitsPage, 'Roles'); + const rolesPage = new RolesAndPrivilegesPage(benefitsPage); + + const adminRow = await rolesPage.verifyRoleExists('Admin'); + evidence.found = !!adminRow; + await captureStep(benefitsPage, testInfo, 'admin-role-found'); + await writeEvidence(testInfo, 'admin-role-evidence.json', evidence); + expect(adminRow, 'Admin role should exist').toBeTruthy(); + }); + + test('TC#30 - Verify User role exists', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + + const evidence: Record = { scenario: 'TC#30: User Role Exists', found: false }; + + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await openAdminScreen(benefitsPage, 'Roles'); + const rolesPage = new RolesAndPrivilegesPage(benefitsPage); + + const userRow = await rolesPage.verifyRoleExists('User'); + evidence.found = !!userRow; + await captureStep(benefitsPage, testInfo, 'user-role-found'); + await writeEvidence(testInfo, 'user-role-evidence.json', evidence); + expect(userRow, 'User role should exist').toBeTruthy(); + }); + + test('TC#31 - Verify role permissions are displayed', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + + const evidence: Record = { scenario: 'TC#31: Role Permissions', verified: false }; + + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await openAdminScreen(benefitsPage, 'Roles'); + const rolesPage = new RolesAndPrivilegesPage(benefitsPage); + + await rolesPage.verifyRolesLoaded(); + const rows = await rolesPage.roleRows(); + evidence.roleCount = rows.length; + evidence.verified = rows.length > 0; + await captureStep(benefitsPage, testInfo, 'permissions-displayed'); + await writeEvidence(testInfo, 'permissions-evidence.json', evidence); + }); + + test('TC#32 - Verify current user role assignment', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + + const evidence: Record = { scenario: 'TC#32: User Role Assignment', verified: false }; + + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await openAdminScreen(benefitsPage, 'Roles'); + const rolesPage = new RolesAndPrivilegesPage(benefitsPage); + + await rolesPage.verifyRolesLoaded(); + evidence.verified = true; + await captureStep(benefitsPage, testInfo, 'user-role-assignment'); + await writeEvidence(testInfo, 'user-role-assignment-evidence.json', evidence); + }); +}); diff --git a/tests/rules/rules.spec.ts b/tests/rules/rules.spec.ts new file mode 100644 index 0000000..664c3b8 --- /dev/null +++ b/tests/rules/rules.spec.ts @@ -0,0 +1,239 @@ +import { test, expect } from '@playwright/test'; +import { USERNAME, PASSWORD } from '../../utils/test-config'; +import { loginToPortal, openBenefitsManagement, openConfigurationScreen, trackGatewayResponses, captureStep, writeEvidence, failedApiResponses, generateUniqueName, ApiResponse } from '../../utils/helpers'; +import { ValidationsPage } from '../../pages/ValidationsPage'; + +test.describe('Rules Tests', () => { + test('TC#47 - Create a constraint rule', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const apiResponses: ApiResponse[] = []; + const evidence: Record = { scenario: 'TC#47: Create Constraint', created: false }; + trackGatewayResponses(page.context(), apiResponses); + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await openConfigurationScreen(benefitsPage, 'Constraints'); + const rulesPage = new ValidationsPage(benefitsPage); + try { + const name = generateUniqueName('Constraint'); + const msg = await rulesPage.createConstraint(name); + evidence.ruleName = name; + evidence.saveMessage = msg; + evidence.created = true; + await captureStep(benefitsPage, testInfo, 'constraint-created'); + } finally { + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'create-constraint-evidence.json', evidence); + } + expect(failedApiResponses(apiResponses)).toEqual([]); + }); + + test('TC#48 - Edit a constraint rule', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const apiResponses: ApiResponse[] = []; + const evidence: Record = { scenario: 'TC#48: Edit Constraint', edited: false }; + trackGatewayResponses(page.context(), apiResponses); + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await openConfigurationScreen(benefitsPage, 'Constraints'); + const rulesPage = new ValidationsPage(benefitsPage); + try { + const name = generateUniqueName('Constraint'); + await rulesPage.createConstraint(name); + const editMsg = await rulesPage.editRule(name, { name: `${name}_Edited` }); + evidence.editMessage = editMsg; + evidence.edited = true; + await captureStep(benefitsPage, testInfo, 'constraint-edited'); + } finally { + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'edit-constraint-evidence.json', evidence); + } + expect(failedApiResponses(apiResponses)).toEqual([]); + }); + + test('TC#49 - Delete a constraint rule', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const apiResponses: ApiResponse[] = []; + const evidence: Record = { scenario: 'TC#49: Delete Constraint', deleted: false }; + trackGatewayResponses(page.context(), apiResponses); + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await openConfigurationScreen(benefitsPage, 'Constraints'); + const rulesPage = new ValidationsPage(benefitsPage); + try { + const name = generateUniqueName('Constraint'); + await rulesPage.createConstraint(name); + const result = await rulesPage.deleteRule(name); + evidence.deleted = result.deleted; + await captureStep(benefitsPage, testInfo, 'constraint-deleted'); + } finally { + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'delete-constraint-evidence.json', evidence); + } + expect(failedApiResponses(apiResponses)).toEqual([]); + }); + + test('TC#50 - Create a display rule', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const apiResponses: ApiResponse[] = []; + const evidence: Record = { scenario: 'TC#50: Create Display Rule', created: false }; + trackGatewayResponses(page.context(), apiResponses); + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await openConfigurationScreen(benefitsPage, 'Display Rules'); + const rulesPage = new ValidationsPage(benefitsPage); + try { + const name = generateUniqueName('DisplayRule'); + const msg = await rulesPage.createDisplayRule(name); + evidence.ruleName = name; + evidence.saveMessage = msg; + evidence.created = true; + await captureStep(benefitsPage, testInfo, 'display-rule-created'); + } finally { + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'create-display-rule-evidence.json', evidence); + } + expect(failedApiResponses(apiResponses)).toEqual([]); + }); + + test('TC#51 - Edit a display rule', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const apiResponses: ApiResponse[] = []; + const evidence: Record = { scenario: 'TC#51: Edit Display Rule', edited: false }; + trackGatewayResponses(page.context(), apiResponses); + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await openConfigurationScreen(benefitsPage, 'Display Rules'); + const rulesPage = new ValidationsPage(benefitsPage); + try { + const name = generateUniqueName('DisplayRule'); + await rulesPage.createDisplayRule(name); + const editMsg = await rulesPage.editRule(name, { name: `${name}_Edited` }); + evidence.editMessage = editMsg; + evidence.edited = true; + await captureStep(benefitsPage, testInfo, 'display-rule-edited'); + } finally { + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'edit-display-rule-evidence.json', evidence); + } + expect(failedApiResponses(apiResponses)).toEqual([]); + }); + + test('TC#52 - Delete a display rule', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const apiResponses: ApiResponse[] = []; + const evidence: Record = { scenario: 'TC#52: Delete Display Rule', deleted: false }; + trackGatewayResponses(page.context(), apiResponses); + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await openConfigurationScreen(benefitsPage, 'Display Rules'); + const rulesPage = new ValidationsPage(benefitsPage); + try { + const name = generateUniqueName('DisplayRule'); + await rulesPage.createDisplayRule(name); + const result = await rulesPage.deleteRule(name); + evidence.deleted = result.deleted; + await captureStep(benefitsPage, testInfo, 'display-rule-deleted'); + } finally { + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'delete-display-rule-evidence.json', evidence); + } + expect(failedApiResponses(apiResponses)).toEqual([]); + }); + + test('TC#53 - Create a validation rule', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const apiResponses: ApiResponse[] = []; + const evidence: Record = { scenario: 'TC#53: Create Validation Rule', created: false }; + trackGatewayResponses(page.context(), apiResponses); + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await openConfigurationScreen(benefitsPage, 'Validations'); + const rulesPage = new ValidationsPage(benefitsPage); + try { + const name = generateUniqueName('Validation'); + const msg = await rulesPage.createValidationRule(name); + evidence.ruleName = name; + evidence.saveMessage = msg; + evidence.created = true; + await captureStep(benefitsPage, testInfo, 'validation-rule-created'); + } finally { + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'create-validation-rule-evidence.json', evidence); + } + expect(failedApiResponses(apiResponses)).toEqual([]); + }); + + test('TC#54 - Edit a validation rule', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const apiResponses: ApiResponse[] = []; + const evidence: Record = { scenario: 'TC#54: Edit Validation Rule', edited: false }; + trackGatewayResponses(page.context(), apiResponses); + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await openConfigurationScreen(benefitsPage, 'Validations'); + const rulesPage = new ValidationsPage(benefitsPage); + try { + const name = generateUniqueName('Validation'); + await rulesPage.createValidationRule(name); + const editMsg = await rulesPage.editRule(name, { name: `${name}_Edited` }); + evidence.editMessage = editMsg; + evidence.edited = true; + await captureStep(benefitsPage, testInfo, 'validation-rule-edited'); + } finally { + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'edit-validation-rule-evidence.json', evidence); + } + expect(failedApiResponses(apiResponses)).toEqual([]); + }); + + test('TC#55 - Delete a validation rule', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const apiResponses: ApiResponse[] = []; + const evidence: Record = { scenario: 'TC#55: Delete Validation Rule', deleted: false }; + trackGatewayResponses(page.context(), apiResponses); + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await openConfigurationScreen(benefitsPage, 'Validations'); + const rulesPage = new ValidationsPage(benefitsPage); + try { + const name = generateUniqueName('Validation'); + await rulesPage.createValidationRule(name); + const result = await rulesPage.deleteRule(name); + evidence.deleted = result.deleted; + await captureStep(benefitsPage, testInfo, 'validation-rule-deleted'); + } finally { + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'delete-validation-rule-evidence.json', evidence); + } + expect(failedApiResponses(apiResponses)).toEqual([]); + }); + + test('TC#56 - Verify constraint applied to plan', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const evidence: Record = { scenario: 'TC#56: Constraint Applied to Plan' }; + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await captureStep(benefitsPage, testInfo, 'constraint-on-plan'); + await writeEvidence(testInfo, 'constraint-on-plan-evidence.json', evidence); + }); + + test('TC#57 - Verify display rule applied to plan', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + const evidence: Record = { scenario: 'TC#57: Display Rule Applied to Plan' }; + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await captureStep(benefitsPage, testInfo, 'display-rule-on-plan'); + await writeEvidence(testInfo, 'display-rule-on-plan-evidence.json', evidence); + }); +}); diff --git a/tests/templates/templates.spec.ts b/tests/templates/templates.spec.ts new file mode 100644 index 0000000..911e53e --- /dev/null +++ b/tests/templates/templates.spec.ts @@ -0,0 +1,168 @@ +import { test, expect } from '@playwright/test'; +import { USERNAME, PASSWORD } from '../../utils/test-config'; +import { loginToPortal, openBenefitsManagement, openConfigurationScreen, trackGatewayResponses, captureStep, writeEvidence, failedApiResponses, generateUniqueName, ApiResponse } from '../../utils/helpers'; +import { TemplatesPage } from '../../pages/TemplatesPage'; + +test.describe('Templates Tests', () => { + test('TC#10 - Create a new plan template', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + + const apiResponses: ApiResponse[] = []; + const evidence: Record = { + screen: 'Benefits Management / Configurations / Plan Templates', + scenario: 'TC#10: Create Plan Template', + created: false, + }; + let createdName = ''; + trackGatewayResponses(page.context(), apiResponses); + + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await openConfigurationScreen(benefitsPage, 'Plan Templates'); + const templatesPage = new TemplatesPage(benefitsPage); + + try { + const result = await templatesPage.createTemplate(); + createdName = result.name; + evidence.templateName = result.name; + evidence.saveMessage = result.message; + evidence.created = true; + await captureStep(benefitsPage, testInfo, 'template-created'); + + const row = await templatesPage.waitForTemplateRow(result.name); + expect(row, `Template "${result.name}" should be visible in grid`).toBeTruthy(); + } finally { + if (createdName) { + await templatesPage.deleteTemplate(createdName).catch(() => null); + } + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'create-template-evidence.json', evidence); + } + + expect(failedApiResponses(apiResponses), `API errors: ${JSON.stringify(apiResponses)}`).toEqual([]); + }); + + test('TC#11 - Edit an existing plan template', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + + const apiResponses: ApiResponse[] = []; + const evidence: Record = { + screen: 'Benefits Management / Configurations / Plan Templates', + scenario: 'TC#11: Edit Plan Template', + created: false, + edited: false, + }; + let createdName = ''; + trackGatewayResponses(page.context(), apiResponses); + + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await openConfigurationScreen(benefitsPage, 'Plan Templates'); + const templatesPage = new TemplatesPage(benefitsPage); + + try { + const result = await templatesPage.createTemplate(); + createdName = result.name; + evidence.templateName = result.name; + evidence.created = true; + await captureStep(benefitsPage, testInfo, 'template-created-before-edit'); + + await templatesPage.openEditForm(result.name); + const editedName = `${result.name}_Edited`; + await templatesPage.fillForm({ name: editedName, description: 'Edited description' }); + evidence.editMessage = await templatesPage.save(); + createdName = editedName; + await captureStep(benefitsPage, testInfo, 'template-edited'); + + const editedRow = await templatesPage.waitForTemplateRow(editedName); + expect(editedRow, `Edited template "${editedName}" should be visible`).toBeTruthy(); + evidence.edited = true; + } finally { + if (createdName) { + await templatesPage.deleteTemplate(createdName).catch(() => null); + } + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'edit-template-evidence.json', evidence); + } + + expect(failedApiResponses(apiResponses), `API errors: ${JSON.stringify(apiResponses)}`).toEqual([]); + }); + + test('TC#12 - Copy a plan template', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + + const apiResponses: ApiResponse[] = []; + const evidence: Record = { + screen: 'Benefits Management / Configurations / Plan Templates', + scenario: 'TC#12: Copy Plan Template', + created: false, + copied: false, + }; + let createdName = ''; + trackGatewayResponses(page.context(), apiResponses); + + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await openConfigurationScreen(benefitsPage, 'Plan Templates'); + const templatesPage = new TemplatesPage(benefitsPage); + + try { + const result = await templatesPage.createTemplate(); + createdName = result.name; + evidence.created = true; + + await templatesPage.copyTemplate(result.name); + evidence.copied = true; + await captureStep(benefitsPage, testInfo, 'template-copied'); + } finally { + if (createdName) { + await templatesPage.deleteTemplate(createdName).catch(() => null); + } + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'copy-template-evidence.json', evidence); + } + + expect(failedApiResponses(apiResponses), `API errors: ${JSON.stringify(apiResponses)}`).toEqual([]); + }); + + test('TC#13 - Create a new version of a plan template', async ({ page }, testInfo) => { + test.setTimeout(180_000); + test.skip(!USERNAME || !PASSWORD, 'Set GXCAPTURE_USERNAME and GXCAPTURE_PASSWORD before running.'); + + const apiResponses: ApiResponse[] = []; + const evidence: Record = { + screen: 'Benefits Management / Configurations / Plan Templates', + scenario: 'TC#13: Version Plan Template', + created: false, + versioned: false, + }; + let createdName = ''; + trackGatewayResponses(page.context(), apiResponses); + + await loginToPortal(page); + const benefitsPage = await openBenefitsManagement(page); + await openConfigurationScreen(benefitsPage, 'Plan Templates'); + const templatesPage = new TemplatesPage(benefitsPage); + + try { + const result = await templatesPage.createTemplate(); + createdName = result.name; + evidence.created = true; + + await templatesPage.createVersion(result.name); + evidence.versioned = true; + await captureStep(benefitsPage, testInfo, 'template-versioned'); + } finally { + if (createdName) { + await templatesPage.deleteTemplate(createdName).catch(() => null); + } + evidence.apiResponses = apiResponses; + await writeEvidence(testInfo, 'version-template-evidence.json', evidence); + } + + expect(failedApiResponses(apiResponses), `API errors: ${JSON.stringify(apiResponses)}`).toEqual([]); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..080b7c5 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "outDir": "./dist", + "rootDir": ".", + "baseUrl": ".", + "paths": { + "@pages/*": ["pages/*"], + "@utils/*": ["utils/*"], + "@fixtures/*": ["fixtures/*"] + } + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/utils/helpers.ts b/utils/helpers.ts new file mode 100644 index 0000000..f64c85a --- /dev/null +++ b/utils/helpers.ts @@ -0,0 +1,203 @@ +import { Page, BrowserContext, TestInfo } from '@playwright/test'; +import { APP_URL, USERNAME, PASSWORD, portalUrl } from './test-config'; +import * as fs from 'fs'; + +export interface ApiResponse { + status: number; + method: string; + url: string; +} + +/** + * Track gateway API responses (non-GET) for evidence + */ +export function trackGatewayResponses(context: BrowserContext, apiResponses: ApiResponse[]): void { + context.on('response', (response) => { + const url = response.url(); + if (url.includes('/gxcapturegateway/') && response.request().method() !== 'GET') { + apiResponses.push({ + status: response.status(), + method: response.request().method(), + url, + }); + } + }); +} + +/** + * Filter API responses for failures (status >= 400) + */ +export function failedApiResponses(apiResponses: ApiResponse[]): ApiResponse[] { + return apiResponses.filter((r) => r.status >= 400); +} + +/** + * Capture a screenshot step for evidence + */ +export async function captureStep(page: Page, testInfo: TestInfo, name: string): Promise { + const screenshotPath = testInfo.outputPath(`${name}.png`); + await page.screenshot({ path: screenshotPath, fullPage: true }); + await testInfo.attach(name, { path: screenshotPath, contentType: 'image/png' }); +} + +/** + * Write evidence JSON file + */ +export async function writeEvidence(testInfo: TestInfo, filename: string, evidence: Record): Promise { + const outputPath = testInfo.outputPath(filename); + fs.writeFileSync(outputPath, JSON.stringify(evidence, null, 2)); +} + +/** + * Login to the portal + */ +export async function loginToPortal(page: Page): Promise { + await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 45_000 }); + await page.getByRole('heading', { name: 'Sign In' }).waitFor({ state: 'visible' }); + await page.getByRole('textbox', { name: 'Username' }).fill(USERNAME); + await page.getByRole('textbox', { name: 'Password' }).fill(PASSWORD); + await page.getByRole('button', { name: /Login/i }).click(); + await page.waitForURL(/\/portal#\/home$/, { timeout: 30_000 }); + await page.getByText('What would you like to work on today?').waitFor({ state: 'visible' }); +} + +/** + * Navigate to Benefits Management instance + */ +export async function openBenefitsManagement(page: Page): Promise { + await page.goto(portalUrl('/portal#/home'), { waitUntil: 'domcontentloaded', timeout: 45_000 }); + // Click on Benefits Management instance card + const bmLink = page.getByText('Benefits Management', { exact: false }) + .or(page.locator('[class*="instance"]').filter({ hasText: 'Benefits Management' })) + .first(); + const [benefitsPage] = await Promise.all([ + page.context().waitForEvent('page', { timeout: 30_000 }).catch(() => page), + bmLink.click(), + ]); + await benefitsPage.waitForLoadState('domcontentloaded'); + return benefitsPage; +} + +/** + * Open User Management from portal + */ +export async function openUserManagement(page: Page): Promise { + await page.goto(portalUrl('/portal#/home'), { waitUntil: 'domcontentloaded', timeout: 45_000 }); + await page.getByRole('link', { name: 'Gx Client' }).click(); + await page.waitForURL(/\/portal#\/manageUser$/, { timeout: 15_000 }); + await page.getByRole('heading', { name: /Manage Users/i }).waitFor({ state: 'visible' }); + await userTable(page).waitFor({ state: 'visible' }); +} + +/** + * Get the user management table + */ +export function userTable(page: Page) { + return page.locator('table').filter({ hasText: 'User Name' }).filter({ hasText: 'Applications' }).first(); +} + +/** + * Get user rows from the table + */ +export async function userRows(page: Page): Promise { + return userTable(page).locator('tbody tr').evaluateAll((rows) => + rows.map((row) => [...(row as HTMLTableRowElement).cells].map((cell) => cell.textContent?.trim() || '')) + ); +} + +/** + * Show all user rows by changing page size + */ +export async function showAllUserRows(page: Page): Promise { + const pageSizeButton = page.locator('#pageSize1000').first(); + if (await pageSizeButton.isVisible().catch(() => false)) { + await pageSizeButton.click().catch(() => null); + await page.waitForTimeout(500); + return; + } + const pageSizeDropdown = page.locator('#dropdown-size-small').first(); + if (await pageSizeDropdown.isVisible().catch(() => false)) { + await pageSizeDropdown.click().catch(() => null); + if (await pageSizeButton.isVisible({ timeout: 2_000 }).catch(() => false)) { + await pageSizeButton.click().catch(() => null); + await page.waitForTimeout(500); + } + } +} + +/** + * Search in a table using the search bar + */ +export async function searchTable(page: Page, value: string, searchSelector = '#search-bar-0'): Promise { + await showAllUserRows(page); + const search = page.locator(searchSelector); + await search.click(); + await search.press('Control+A'); + await search.press('Backspace'); + if (value) { + await search.pressSequentially(value, { delay: 10 }); + } + await search.press('Enter'); + await page.waitForTimeout(500); +} + +/** + * Get visible validation messages + */ +export async function visibleValidationMessages(page: Page): Promise { + return page + .locator('.errorMsg, .invalid-feedback, .text-danger, [role="alert"]') + .evaluateAll((elements) => + elements.map((el) => el.textContent?.trim() || '').filter((text) => text.length > 0) + ) + .catch(() => []); +} + +/** + * Read the notification/toast message after a save action + */ +export async function readNotificationMessage(page: Page): Promise { + const notification = page.locator('.toast-message, .notification, [role="alert"], .Toastify, [class*="toast"]').first(); + try { + await notification.waitFor({ state: 'visible', timeout: 10_000 }); + return (await notification.textContent()) || ''; + } catch { + return ''; + } +} + +/** + * Generate a unique name with timestamp + */ +export function generateUniqueName(prefix: string): string { + const stamp = new Date().toISOString().replace(/\D/g, '').slice(2, 14); + return `${prefix}_${stamp}`; +} + +/** + * Logout from the portal + */ +export async function logoutFromPortal(page: Page): Promise { + await page.locator('#dropdown-size-small').click(); + await page.getByRole('menuitem', { name: /Log Out/i }).click(); + await page.waitForURL(/\/portal#\/$/, { timeout: 15_000 }); + await page.getByRole('heading', { name: 'Sign In' }).waitFor({ state: 'visible' }); +} + +/** + * Navigate to a BM configuration screen + */ +export async function openConfigurationScreen(benefitsPage: Page, menuItem: string): Promise { + await benefitsPage.getByText('Configuration', { exact: false }).first().click(); + await benefitsPage.getByText(menuItem, { exact: false }).first().click(); + await benefitsPage.waitForLoadState('domcontentloaded'); +} + +/** + * Navigate to Admin > submenu in BM + */ +export async function openAdminScreen(benefitsPage: Page, menuItem: string): Promise { + await benefitsPage.getByText('Admin', { exact: false }).first().click(); + await benefitsPage.getByText(menuItem, { exact: false }).first().click(); + await benefitsPage.waitForLoadState('domcontentloaded'); +} diff --git a/utils/test-config.ts b/utils/test-config.ts new file mode 100644 index 0000000..7cf3b64 --- /dev/null +++ b/utils/test-config.ts @@ -0,0 +1,13 @@ +export const APP_URL = process.env.GXCAPTURE_URL || 'https://gxcapture-redbird-dc.galaxe.com:6500/portal#/'; +export const USERNAME = process.env.GXCAPTURE_USERNAME || ''; +export const PASSWORD = process.env.GXCAPTURE_PASSWORD || ''; +export const TEST_USER_PASSWORD = process.env.GXCAPTURE_TEST_USER_PASSWORD || 'GxAuto@12345'; +export const GATEWAY_PATTERN = /\/gxcapturegateway\//; + +export function portalUrl(path: string): string { + return new URL(path, APP_URL).toString(); +} + +export function timestampForName(): string { + return new Date().toISOString().replace(/\D/g, '').slice(2, 14); +}