diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 00000000..f0da47cb --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,121 @@ +name: E2E Tests + +on: + push: + branches: ['19.0'] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + e2e: + runs-on: ubuntu-latest + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + spec: + - tests/01-spp-starter-spmis.spec.ts + - tests/02-spp-starter-farmer-registry.spec.ts + + steps: + - uses: actions/checkout@v4 + + - name: Start OpenSPP stack + run: docker compose --profile ui up -d --build + + - name: Wait for OpenSPP to be healthy + run: | + echo "Waiting for http://localhost:8069/web/health ..." + for i in $(seq 1 30); do + if curl -sf http://localhost:8069/web/health; then + echo "OpenSPP is ready" + exit 0 + fi + echo "Attempt $i/30 — sleeping 10s" + sleep 10 + done + echo "OpenSPP did not become healthy in time" + docker compose --profile ui logs + exit 1 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: e2e/package.json + + - name: Install Playwright + working-directory: e2e + run: | + npm install + npx playwright install chromium --with-deps + + - name: Run E2E tests + working-directory: e2e + env: + ODOO_URL: http://localhost:8069 + run: npx playwright test ${{ matrix.spec }} + + - name: Upload test report + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-report-${{ strategy.job-index }} + path: e2e/reports/html + retention-days: 7 + + - name: Record spec result + if: always() + run: | + case "${{ matrix.spec }}" in + *spmis*) NAME="SP-MIS" ;; + *farmer*) NAME="Farmer Registry" ;; + *) NAME="${{ matrix.spec }}" ;; + esac + mkdir -p status + echo "{\"name\": \"$NAME\", \"result\": \"${{ job.status }}\"}" > status/result.json + + - name: Upload spec result + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-status-${{ strategy.job-index }} + path: status/result.json + + notify: + needs: e2e + if: always() + runs-on: ubuntu-latest + steps: + - name: Download spec results + uses: actions/download-artifact@v4 + with: + pattern: test-status-* + path: statuses + merge-multiple: true + + - name: Post result to Discord + run: | + COLOR=3066993 + if [ "${{ needs.e2e.result }}" != "success" ]; then COLOR=15158332; fi + + FIELDS="[]" + for f in statuses/*.json; do + NAME=$(jq -r .name "$f") + RESULT=$(jq -r .result "$f") + if [ "$RESULT" == "success" ]; then EMOJI="✅"; else EMOJI="❌"; fi + FIELDS=$(echo "$FIELDS" | jq --arg n "$NAME" --arg v "$RESULT $EMOJI" '. + [{"name": $n, "value": $v, "inline": false}]') + done + + PAYLOAD=$(jq -n \ + --arg title "OpenSPP E2E Tests" \ + --arg url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ + --argjson color "$COLOR" \ + --argjson fields "$FIELDS" \ + '{embeds: [{title: $title, url: $url, color: $color, fields: $fields}]}') + + curl -sf -H "Content-Type: application/json" -X POST -d "$PAYLOAD" "${{ secrets.DISCORD_WEBHOOK }}" diff --git a/.gitignore b/.gitignore index 896e26fa..fdcf476f 100644 --- a/.gitignore +++ b/.gitignore @@ -111,3 +111,9 @@ docker/fluentd/log/ .actrc scripts/run_ci_local.sh .github/workflows/ci-local.yml + +# E2E tests (Playwright) +e2e/node_modules/ +e2e/test-results/ +e2e/playwright-report/ +e2e/blob-report/ diff --git a/e2e/CLAUDE.md b/e2e/CLAUDE.md new file mode 100644 index 00000000..dbe6e6b3 --- /dev/null +++ b/e2e/CLAUDE.md @@ -0,0 +1,75 @@ +# E2E Tests + +Playwright-based end-to-end tests for OpenSPP. + +## Prerequisites + +- Docker running with WSL Ubuntu +- Node.js 20+ installed +- Playwright and Chromium installed + +## First-time setup + +```bash +cd e2e +npm install +npx playwright install chromium --with-deps +``` + +## Running tests + +```bash +cd e2e +npx playwright test --headed +``` + +Tests run sequentially (1 worker). Each test file resets the database before running. + +## Test files + +| File | Module | Description | +|------|--------|-------------| +| `01-spp-starter-spmis.spec.ts` | `spp_starter_sp_mis` | Installs OpenSPP Starter SP-MIS and verifies nav menus | +| `02-spp-starter-farmer-registry.spec.ts` | `spp_starter_farmer_registry` | Installs OpenSPP Starter Farmer Registry and verifies nav menus | + +## How each test works + +1. `beforeAll` — tears down and rebuilds a fresh Docker stack via `docker compose --profile ui down -v && docker compose --profile ui up -d` +2. Waits for Odoo to be healthy at `http://localhost:8069/web/health` +3. Logs in as `admin` / `admin` +4. Goes to Apps, removes the preset filter, searches by module technical name +5. Clicks Activate and waits for installation to complete +6. Waits 1 minute for nav menus to settle +7. Verifies nav menus are present and takes a screenshot + +## Why `docker compose down -v && up` instead of `spp resetdb` + +The `spp` CLI has a `resetdb` command that resets only the database (faster, keeps containers running). However, this workflow is designed to run automatically on every merge to `branch 19.0` via GitHub Actions. In that context, a full `docker compose down -v && up` is intentional because: + +- It pulls and uses the latest image built from the merged code +- It guarantees no leftover state from previous runs (volumes, cached data) +- It ensures every test run reflects exactly what was merged + +Use `spp resetdb` for local dev iteration only. Do not replace the `docker compose` reset in `helpers.ts` for CI purposes. + +## Adding new tests + +When adding a new test to any spec file: + +1. Use `test.describe.serial` and share a single `page` across all tests via `let page: Page` at the describe scope +2. Number tests sequentially (`01`, `02`, `03` …) so the report shows them in order +3. **Always update the comment block at the top of the spec file** to include the new test — one line per test in the format: + ``` + // NN - Plain-language description of what the test does + ``` +4. Use codegen to record actions (`npx playwright codegen http://localhost:8069`), then polish the output — remove redundant clicks, replace CSS locators with role/label/placeholder locators, add `waitForLoadState`, and add `expect` assertions to verify the result was saved + +## Reports + +After a run, open the HTML report: +```bash +cd e2e +npm run report +``` + +Screenshots on failure and post-install screenshots are saved in `e2e/reports/`. diff --git a/e2e/Dockerfile b/e2e/Dockerfile new file mode 100644 index 00000000..591812a2 --- /dev/null +++ b/e2e/Dockerfile @@ -0,0 +1,10 @@ +FROM mcr.microsoft.com/playwright:v1.44.0-jammy + +WORKDIR /app + +COPY package.json ./ +RUN npm install + +COPY . . + +CMD ["npx", "playwright", "test"] diff --git a/e2e/fixtures/sample_BIR_2316.pdf b/e2e/fixtures/sample_BIR_2316.pdf new file mode 100644 index 00000000..f913659d Binary files /dev/null and b/e2e/fixtures/sample_BIR_2316.pdf differ diff --git a/e2e/fixtures/sample_academic_calendar.pdf b/e2e/fixtures/sample_academic_calendar.pdf new file mode 100644 index 00000000..a0c472aa Binary files /dev/null and b/e2e/fixtures/sample_academic_calendar.pdf differ diff --git a/e2e/fixtures/sample_authorization_letter.pdf b/e2e/fixtures/sample_authorization_letter.pdf new file mode 100644 index 00000000..a39885ae Binary files /dev/null and b/e2e/fixtures/sample_authorization_letter.pdf differ diff --git a/e2e/fixtures/sample_cert_of_enrolment.pdf b/e2e/fixtures/sample_cert_of_enrolment.pdf new file mode 100644 index 00000000..08f4c03a Binary files /dev/null and b/e2e/fixtures/sample_cert_of_enrolment.pdf differ diff --git a/e2e/fixtures/sample_valid_ID_parent.pdf b/e2e/fixtures/sample_valid_ID_parent.pdf new file mode 100644 index 00000000..9167b82f Binary files /dev/null and b/e2e/fixtures/sample_valid_ID_parent.pdf differ diff --git a/e2e/package-lock.json b/e2e/package-lock.json new file mode 100644 index 00000000..043f60fd --- /dev/null +++ b/e2e/package-lock.json @@ -0,0 +1,96 @@ +{ + "name": "openspp-e2e", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "openspp-e2e", + "version": "1.0.0", + "devDependencies": { + "@playwright/test": "^1.44.0", + "@types/node": "^25.7.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "25.7.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.7.0.tgz", + "integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.21.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/undici-types": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.21.0.tgz", + "integrity": "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/e2e/package.json b/e2e/package.json new file mode 100644 index 00000000..426c069f --- /dev/null +++ b/e2e/package.json @@ -0,0 +1,14 @@ +{ + "name": "openspp-e2e", + "version": "1.0.0", + "private": true, + "scripts": { + "test": "playwright test", + "test:headed": "playwright test --headed", + "report": "playwright show-report reports/html" + }, + "devDependencies": { + "@playwright/test": "^1.44.0", + "@types/node": "^25.7.0" + } +} diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts new file mode 100644 index 00000000..51ca794a --- /dev/null +++ b/e2e/playwright.config.ts @@ -0,0 +1,25 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests', + timeout: 600_000, // 10 min — covers 2x app installs + 3 min wait + workers: 1, + retries: 0, + reporter: [['list'], ['html', { outputFolder: 'reports/html', open: 'never' }]], + + use: { + baseURL: process.env.ODOO_URL ?? 'http://localhost:8069', + screenshot: 'only-on-failure', + video: 'retain-on-failure', + launchOptions: { + slowMo: 500, // 0.5s delay between actions + }, + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}); diff --git a/e2e/tests/01-spp-starter-spmis.spec.ts b/e2e/tests/01-spp-starter-spmis.spec.ts new file mode 100644 index 00000000..ee1c2301 --- /dev/null +++ b/e2e/tests/01-spp-starter-spmis.spec.ts @@ -0,0 +1,774 @@ +// OpenSPP Starter SP-MIS — end-to-end test suite +// +// What this tests: +// 01 - Logs in as admin and installs the spp_starter_sp_mis module via the Apps menu +// 02 - Creates individual registrant: Doe, John Miller +// 03 - Creates individual registrant: Reyes, Maria Clara +// 04 - Creates individual registrant: Santos, Jose Miguel +// 05 - Creates individual registrant: Garcia, Ana Liza +// 06 - Creates individual registrant: Cruz, Roberto Paolo +// 07 - Creates group registrant: Doe Family +// 08 - Creates group registrant: Santos Family +// 09 - Assigns individuals to groups (Doe Family: 02–04, Santos Family: 05–06) +// 10 - Creates area hierarchy: Philippines > Manila, Cebu > Mandaue City +// 11 - Assigns an area to each individual registrant +// 12 - Assigns an area to each group registrant +// 13 - Creates an Approval Definition (Change request → HQ validator) and assigns it as the Approval Workflow for Edit Individual Information, Edit Group Information, and Update ID Document change request types +// 14 - Manually defines 5 Change Request Document Type vocabulary codes for the Philippines (PSA Birth Certificate, PhilSys National ID, Barangay Certificate of Residency, PSA Marriage Certificate, Proof of Income), matching what spp_mis_demo_v2's demo generator seeds for "phl", by adding rows to the existing vocabulary's Codes tab via Settings > Vocabularies +// 15 - Manually defines 5 more Change Request Document Type vocabulary codes for the Philippines (BIR Form 2316, Academic Calendar, Authorization Letter, Certificate of Enrolment, Valid ID of Parent), matching the e2e fixture PDFs in e2e/fixtures/, by adding rows to the same vocabulary's Codes tab as test 14 +// +// All tests run in order and share a single browser session (test.describe.serial). +// A fresh Docker stack is spun up in beforeAll so every run starts from a clean database. + +import { test, expect, Page, Browser } from '@playwright/test'; +import { resetStack } from './helpers'; + +async function login(page: Page) { + await page.goto('/web/login'); + await page.getByRole('textbox', { name: 'Email' }).fill('admin'); + await page.getByRole('textbox', { name: 'Password' }).fill('admin'); + await page.getByRole('button', { name: 'Log in' }).click(); + await expect(page.locator('.o_main_navbar')).toBeVisible({ timeout: 30_000 }); +} + +async function installApp(page: Page, technicalName: string) { + console.log('✅ Clicking Apps menuitem'); + await page.getByRole('menuitem', { name: 'Apps' }).click(); + console.log('✅ Clicked Apps, waiting for networkidle'); + await page.waitForLoadState('domcontentloaded'); + console.log('✅ Apps page loaded'); + + // Remove all preset filter chips (there are two by default) + await page.getByRole('button', { name: 'Remove' }).click(); + console.log('✅ Clicked Remove button'); + await page.waitForLoadState('domcontentloaded'); + console.log('✅ Filter cleared, page settled'); + + // Search by technical module name + console.log('✅ Filling search box'); + await page.getByRole('searchbox', { name: 'Search...' }).fill(technicalName); + console.log('✅ Pressing Enter to search'); + await page.getByRole('searchbox', { name: 'Search...' }).press('Enter'); + await page.waitForLoadState('domcontentloaded'); + console.log('✅ Search done, looking for Install button'); + + // Click Install on the matching card + const installBtn = page.getByRole('button', { name: 'Activate' }).first(); + await expect(installBtn).toBeVisible({ timeout: 15_000 }); + console.log('✅ Install button found, clicking'); + await installBtn.click(); + + // Wait for installation — Odoo shows a loading spinner then redirects away from apps + console.log('✅ Waiting for installation to complete'); + await page.waitForURL(/^(?!.*action-178).*/, { timeout: 180_000 }); + await page.waitForLoadState('domcontentloaded', { timeout: 180_000 }); + + // Wait for Odoo loading spinner to fully disappear + await page.locator('.o_loading').waitFor({ state: 'hidden', timeout: 180_000 }).catch(() => {}); + + // Wait for at least one nav menu item to appear + await expect(page.locator('.o_main_navbar .o_menu_sections .o_nav_entry').first()).toBeVisible({ timeout: 180_000 }); + console.log('✅ Installation complete — nav menus are visible'); +} + +test.describe.serial('OpenSPP Starter SP-MIS', () => { + let page: Page; + + test.beforeAll(async ({ browser }) => { + await resetStack(); + page = await browser.newPage(); + }); + + test.afterAll(async () => { + await page.close(); + }); + + test('01 - login and install OpenSPP Starter SP-MIS', async () => { + await login(page); + await installApp(page, 'spp_starter_sp_mis'); + + console.log('⏳ Giving the system 30 seconds to fully initialize all menu items before proceeding...'); + await page.waitForTimeout(30_000); + + await expect(page.locator('.o_main_navbar')).toBeVisible(); + const menuItems = page.locator('.o_main_navbar .o_nav_entry, .o_main_navbar .o_dropdown'); + await expect(menuItems.first()).toBeVisible({ timeout: 10_000 }); + + await page.screenshot({ path: 'reports/post-install-navbar.png', fullPage: false }); + }); + + test('02 - create individual registrant', async () => { + await page.getByRole('menuitem', { name: 'Registry' }).nth(1).click(); + await page.waitForLoadState('domcontentloaded'); + await page.getByRole('button', { name: 'New Individual' }).click(); + await page.waitForLoadState('domcontentloaded'); + + await page.getByRole('textbox', { name: 'Family Name?' }).fill('Doe'); + await page.getByRole('textbox', { name: 'Given Name?' }).fill('John Miller'); + await page.getByRole('textbox', { name: 'Date of Birth?' }).fill('01/01/1975'); + await page.getByRole('textbox', { name: 'Date of Birth?' }).press('Escape'); + console.log('✅ Basic info filled'); + + await page.getByRole('combobox', { name: 'Gender' }).click(); + await page.getByRole('option', { name: 'Male', exact: true }).click(); + await page.getByRole('combobox', { name: 'Civil Status?' }).click(); + await page.getByRole('option', { name: 'Married', exact: true }).click(); + await page.getByRole('textbox', { name: 'Income' }).fill('10000'); + await page.getByRole('combobox', { name: 'Occupation?' }).click(); + await page.getByRole('option', { name: 'Armed Forces Occupations', exact: true }).click(); + console.log('✅ Demographics filled'); + + await page.getByRole('textbox', { name: 'Email?' }).fill('johnmiller@mail.com'); + await page.getByRole('button', { name: 'Add a line' }).first().click(); + await page.locator('input[type="tel"]').fill('09772603521'); + await page.getByRole('row', { name: 'Delete row' }).getByRole('combobox').fill('ph'); + await page.getByRole('option', { name: 'Philippines' }).click(); + console.log('✅ Contact info filled'); + + await page.getByRole('button', { name: 'Save manually' }).click(); + await expect(page.locator('.o_form_view')).toBeVisible(); + console.log('✅ Individual registrant saved — verifying all fields...'); + + await expect(page.getByRole('textbox', { name: 'Family Name?' })).toHaveValue('Doe'); + console.log('✅ Family Name: Doe'); + + await expect(page.getByRole('textbox', { name: 'Given Name?' })).toHaveValue('John Miller'); + console.log('✅ Given Name: John Miller'); + + await expect(page.getByRole('button', { name: 'Date of Birth?' })).toContainText('Jan 1, 1975'); + console.log('✅ Date of Birth: Jan 1, 1975'); + + await expect(page.getByRole('combobox', { name: 'Gender' })).toHaveValue('Male'); + console.log('✅ Gender: Male'); + + await expect(page.getByRole('combobox', { name: 'Civil Status?' })).toHaveValue('Married'); + console.log('✅ Civil Status: Married'); + + await expect(page.getByRole('textbox', { name: 'Income' })).toHaveValue('10,000.00'); + console.log('✅ Income: 10,000.00'); + + await expect(page.getByRole('textbox', { name: 'Email?' })).toHaveValue('johnmiller@mail.com'); + console.log('✅ Email: johnmiller@mail.com'); + + await expect(page.getByRole('link', { name: '09772603521' })).toBeVisible(); + console.log('✅ Phone: 09772603521'); + + console.log('✅ All fields verified — individual registrant saved successfully'); + + await page.getByRole('button', { name: 'Browse All (Audit)' }).click(); + await page.getByRole('menuitem', { name: 'All Individuals' }).click(); + await page.waitForLoadState('domcontentloaded'); + await expect(page.getByRole('cell', { name: 'DOE, JOHN MILLER' })).toBeVisible({ timeout: 10_000 }); + console.log('✅ Registrant "DOE, JOHN MILLER" verified in Browse All Individuals list'); + + await page.getByRole('cell', { name: 'DOE, JOHN MILLER' }).click(); + await page.waitForLoadState('domcontentloaded'); + await expect(page.locator('.o_form_view')).toBeVisible({ timeout: 10_000 }); + await expect(page.getByRole('textbox', { name: 'Family Name?' })).toHaveValue('Doe'); + console.log('✅ Individual registrant "DOE, JOHN MILLER" created via GUI successfully'); + }); + + test('03 - create individual registrant: Reyes, Maria Clara', async () => { + await page.getByRole('menuitem', { name: 'Registry' }).nth(1).click(); + await page.waitForLoadState('domcontentloaded'); + await page.getByRole('button', { name: 'New Individual' }).click(); + await page.waitForLoadState('domcontentloaded'); + + await page.getByRole('textbox', { name: 'Family Name?' }).fill('Reyes'); + await page.getByRole('textbox', { name: 'Given Name?' }).fill('Maria Clara'); + await page.getByRole('textbox', { name: 'Date of Birth?' }).fill('03/15/1985'); + await page.getByRole('textbox', { name: 'Date of Birth?' }).press('Escape'); + console.log('✅ Basic info filled'); + + await page.getByRole('combobox', { name: 'Gender' }).click(); + await page.getByRole('option', { name: 'Female', exact: true }).click(); + await page.getByRole('combobox', { name: 'Civil Status?' }).click(); + await page.getByRole('option', { name: 'Married', exact: true }).click(); + await page.getByRole('textbox', { name: 'Income' }).fill('15000'); + await page.getByRole('combobox', { name: 'Occupation?' }).click(); + await page.getByRole('option', { name: 'Armed Forces Occupations', exact: true }).click(); + console.log('✅ Demographics filled'); + + await page.getByRole('textbox', { name: 'Email?' }).fill('mariaclara@mail.com'); + await page.getByRole('button', { name: 'Add a line' }).first().click(); + await page.locator('input[type="tel"]').fill('09123456789'); + await page.getByRole('row', { name: 'Delete row' }).getByRole('combobox').fill('ph'); + await page.getByRole('option', { name: 'Philippines' }).click(); + console.log('✅ Contact info filled'); + + await page.getByRole('button', { name: 'Save manually' }).click(); + await expect(page.locator('.o_form_view')).toBeVisible(); + console.log('✅ Individual registrant saved — verifying all fields...'); + + await expect(page.getByRole('textbox', { name: 'Family Name?' })).toHaveValue('Reyes'); + await expect(page.getByRole('textbox', { name: 'Given Name?' })).toHaveValue('Maria Clara'); + await expect(page.getByRole('button', { name: 'Date of Birth?' })).toContainText('Mar 15, 1985'); + await expect(page.getByRole('combobox', { name: 'Gender' })).toHaveValue('Female'); + await expect(page.getByRole('combobox', { name: 'Civil Status?' })).toHaveValue('Married'); + await expect(page.getByRole('textbox', { name: 'Income' })).toHaveValue('15,000.00'); + await expect(page.getByRole('textbox', { name: 'Email?' })).toHaveValue('mariaclara@mail.com'); + await expect(page.getByRole('link', { name: '09123456789' })).toBeVisible(); + console.log('✅ All fields verified — REYES, MARIA CLARA saved successfully'); + + await page.getByRole('button', { name: 'Browse All (Audit)' }).click(); + await page.getByRole('menuitem', { name: 'All Individuals' }).click(); + await page.waitForLoadState('domcontentloaded'); + await expect(page.getByRole('cell', { name: 'REYES, MARIA CLARA' })).toBeVisible({ timeout: 10_000 }); + console.log('✅ Individual registrant "REYES, MARIA CLARA" created via GUI successfully'); + }); + + test('04 - create individual registrant: Santos, Jose Miguel', async () => { + await page.getByRole('menuitem', { name: 'Registry' }).nth(1).click(); + await page.waitForLoadState('domcontentloaded'); + await page.getByRole('button', { name: 'New Individual' }).click(); + await page.waitForLoadState('domcontentloaded'); + + await page.getByRole('textbox', { name: 'Family Name?' }).fill('Santos'); + await page.getByRole('textbox', { name: 'Given Name?' }).fill('Jose Miguel'); + await page.getByRole('textbox', { name: 'Date of Birth?' }).fill('07/22/1990'); + await page.getByRole('textbox', { name: 'Date of Birth?' }).press('Escape'); + console.log('✅ Basic info filled'); + + await page.getByRole('combobox', { name: 'Gender' }).click(); + await page.getByRole('option', { name: 'Male', exact: true }).click(); + await page.getByRole('combobox', { name: 'Civil Status?' }).click(); + await page.getByRole('option', { name: 'Married', exact: true }).click(); + await page.getByRole('textbox', { name: 'Income' }).fill('20000'); + await page.getByRole('combobox', { name: 'Occupation?' }).click(); + await page.getByRole('option', { name: 'Armed Forces Occupations', exact: true }).click(); + console.log('✅ Demographics filled'); + + await page.getByRole('textbox', { name: 'Email?' }).fill('josemiguel@mail.com'); + await page.getByRole('button', { name: 'Add a line' }).first().click(); + await page.locator('input[type="tel"]').fill('09234567890'); + await page.getByRole('row', { name: 'Delete row' }).getByRole('combobox').fill('ph'); + await page.getByRole('option', { name: 'Philippines' }).click(); + console.log('✅ Contact info filled'); + + await page.getByRole('button', { name: 'Save manually' }).click(); + await expect(page.locator('.o_form_view')).toBeVisible(); + console.log('✅ Individual registrant saved — verifying all fields...'); + + await expect(page.getByRole('textbox', { name: 'Family Name?' })).toHaveValue('Santos'); + await expect(page.getByRole('textbox', { name: 'Given Name?' })).toHaveValue('Jose Miguel'); + await expect(page.getByRole('button', { name: 'Date of Birth?' })).toContainText('Jul 22, 1990'); + await expect(page.getByRole('combobox', { name: 'Gender' })).toHaveValue('Male'); + await expect(page.getByRole('combobox', { name: 'Civil Status?' })).toHaveValue('Married'); + await expect(page.getByRole('textbox', { name: 'Income' })).toHaveValue('20,000.00'); + await expect(page.getByRole('textbox', { name: 'Email?' })).toHaveValue('josemiguel@mail.com'); + await expect(page.getByRole('link', { name: '09234567890' })).toBeVisible(); + console.log('✅ All fields verified — SANTOS, JOSE MIGUEL saved successfully'); + + await page.getByRole('button', { name: 'Browse All (Audit)' }).click(); + await page.getByRole('menuitem', { name: 'All Individuals' }).click(); + await page.waitForLoadState('domcontentloaded'); + await expect(page.getByRole('cell', { name: 'SANTOS, JOSE MIGUEL' })).toBeVisible({ timeout: 10_000 }); + console.log('✅ Individual registrant "SANTOS, JOSE MIGUEL" created via GUI successfully'); + }); + + test('05 - create individual registrant: Garcia, Ana Liza', async () => { + await page.getByRole('menuitem', { name: 'Registry' }).nth(1).click(); + await page.waitForLoadState('domcontentloaded'); + await page.getByRole('button', { name: 'New Individual' }).click(); + await page.waitForLoadState('domcontentloaded'); + + await page.getByRole('textbox', { name: 'Family Name?' }).fill('Garcia'); + await page.getByRole('textbox', { name: 'Given Name?' }).fill('Ana Liza'); + await page.getByRole('textbox', { name: 'Date of Birth?' }).fill('11/08/1968'); + await page.getByRole('textbox', { name: 'Date of Birth?' }).press('Escape'); + console.log('✅ Basic info filled'); + + await page.getByRole('combobox', { name: 'Gender' }).click(); + await page.getByRole('option', { name: 'Female', exact: true }).click(); + await page.getByRole('combobox', { name: 'Civil Status?' }).click(); + await page.getByRole('option', { name: 'Married', exact: true }).click(); + await page.getByRole('textbox', { name: 'Income' }).fill('8000'); + await page.getByRole('combobox', { name: 'Occupation?' }).click(); + await page.getByRole('option', { name: 'Armed Forces Occupations', exact: true }).click(); + console.log('✅ Demographics filled'); + + await page.getByRole('textbox', { name: 'Email?' }).fill('analiza@mail.com'); + await page.getByRole('button', { name: 'Add a line' }).first().click(); + await page.locator('input[type="tel"]').fill('09345678901'); + await page.getByRole('row', { name: 'Delete row' }).getByRole('combobox').fill('ph'); + await page.getByRole('option', { name: 'Philippines' }).click(); + console.log('✅ Contact info filled'); + + await page.getByRole('button', { name: 'Save manually' }).click(); + await expect(page.locator('.o_form_view')).toBeVisible(); + console.log('✅ Individual registrant saved — verifying all fields...'); + + await expect(page.getByRole('textbox', { name: 'Family Name?' })).toHaveValue('Garcia'); + await expect(page.getByRole('textbox', { name: 'Given Name?' })).toHaveValue('Ana Liza'); + await expect(page.getByRole('button', { name: 'Date of Birth?' })).toContainText('Nov 8, 1968'); + await expect(page.getByRole('combobox', { name: 'Gender' })).toHaveValue('Female'); + await expect(page.getByRole('combobox', { name: 'Civil Status?' })).toHaveValue('Married'); + await expect(page.getByRole('textbox', { name: 'Income' })).toHaveValue('8,000.00'); + await expect(page.getByRole('textbox', { name: 'Email?' })).toHaveValue('analiza@mail.com'); + await expect(page.getByRole('link', { name: '09345678901' })).toBeVisible(); + console.log('✅ All fields verified — GARCIA, ANA LIZA saved successfully'); + + await page.getByRole('button', { name: 'Browse All (Audit)' }).click(); + await page.getByRole('menuitem', { name: 'All Individuals' }).click(); + await page.waitForLoadState('domcontentloaded'); + await expect(page.getByRole('cell', { name: 'GARCIA, ANA LIZA' })).toBeVisible({ timeout: 10_000 }); + console.log('✅ Individual registrant "GARCIA, ANA LIZA" created via GUI successfully'); + }); + + test('06 - create individual registrant: Cruz, Roberto Paolo', async () => { + await page.getByRole('menuitem', { name: 'Registry' }).nth(1).click(); + await page.waitForLoadState('domcontentloaded'); + await page.getByRole('button', { name: 'New Individual' }).click(); + await page.waitForLoadState('domcontentloaded'); + + await page.getByRole('textbox', { name: 'Family Name?' }).fill('Cruz'); + await page.getByRole('textbox', { name: 'Given Name?' }).fill('Roberto Paolo'); + await page.getByRole('textbox', { name: 'Date of Birth?' }).fill('04/30/1978'); + await page.getByRole('textbox', { name: 'Date of Birth?' }).press('Escape'); + console.log('✅ Basic info filled'); + + await page.getByRole('combobox', { name: 'Gender' }).click(); + await page.getByRole('option', { name: 'Male', exact: true }).click(); + await page.getByRole('combobox', { name: 'Civil Status?' }).click(); + await page.getByRole('option', { name: 'Married', exact: true }).click(); + await page.getByRole('textbox', { name: 'Income' }).fill('12000'); + await page.getByRole('combobox', { name: 'Occupation?' }).click(); + await page.getByRole('option', { name: 'Armed Forces Occupations', exact: true }).click(); + console.log('✅ Demographics filled'); + + await page.getByRole('textbox', { name: 'Email?' }).fill('robertopaolo@mail.com'); + await page.getByRole('button', { name: 'Add a line' }).first().click(); + await page.locator('input[type="tel"]').fill('09456789012'); + await page.getByRole('row', { name: 'Delete row' }).getByRole('combobox').fill('ph'); + await page.getByRole('option', { name: 'Philippines' }).click(); + console.log('✅ Contact info filled'); + + await page.getByRole('button', { name: 'Save manually' }).click(); + await expect(page.locator('.o_form_view')).toBeVisible(); + console.log('✅ Individual registrant saved — verifying all fields...'); + + await expect(page.getByRole('textbox', { name: 'Family Name?' })).toHaveValue('Cruz'); + await expect(page.getByRole('textbox', { name: 'Given Name?' })).toHaveValue('Roberto Paolo'); + await expect(page.getByRole('button', { name: 'Date of Birth?' })).toContainText('Apr 30, 1978'); + await expect(page.getByRole('combobox', { name: 'Gender' })).toHaveValue('Male'); + await expect(page.getByRole('combobox', { name: 'Civil Status?' })).toHaveValue('Married'); + await expect(page.getByRole('textbox', { name: 'Income' })).toHaveValue('12,000.00'); + await expect(page.getByRole('textbox', { name: 'Email?' })).toHaveValue('robertopaolo@mail.com'); + await expect(page.getByRole('link', { name: '09456789012' })).toBeVisible(); + console.log('✅ All fields verified — CRUZ, ROBERTO PAOLO saved successfully'); + + await page.getByRole('button', { name: 'Browse All (Audit)' }).click(); + await page.getByRole('menuitem', { name: 'All Individuals' }).click(); + await page.waitForLoadState('domcontentloaded'); + await expect(page.getByRole('cell', { name: 'CRUZ, ROBERTO PAOLO' })).toBeVisible({ timeout: 10_000 }); + console.log('✅ Individual registrant "CRUZ, ROBERTO PAOLO" created via GUI successfully'); + }); + + test('07 - create group registrant', async () => { + await page.getByRole('menuitem', { name: 'Registry' }).nth(1).click(); + await page.waitForLoadState('domcontentloaded'); + await page.getByRole('button', { name: 'New Group' }).click(); + await page.waitForLoadState('domcontentloaded'); + + await page.getByRole('textbox', { name: 'Group Name?' }).fill('Doe Family'); + await page.getByRole('combobox', { name: 'Group Type?' }).click(); + await page.getByRole('option', { name: 'Family' }).click(); + console.log('✅ Group name and type filled'); + + await page.getByRole('textbox', { name: 'Address' }).fill('Sample Address'); + console.log('✅ Address filled'); + + await page.getByRole('textbox', { name: 'Email?' }).fill('Doefamiliy@mail.com'); + await page.getByRole('button', { name: 'Add a line' }).first().click(); + await page.locator('input[type="tel"]').fill('09987654321'); + await page.getByRole('row', { name: 'Delete row' }).getByRole('combobox').fill('ph'); + await page.getByRole('option', { name: 'Philippines' }).click(); + console.log('✅ Contact info filled'); + + await page.getByRole('button', { name: 'Save manually' }).click(); + await expect(page.locator('.o_form_view')).toBeVisible(); + console.log('✅ Group registrant saved — verifying all fields...'); + + await expect(page.getByRole('textbox', { name: 'Group Name?' })).toHaveValue('Doe Family'); + console.log('✅ Group Name: Doe Family'); + + await expect(page.getByRole('combobox', { name: 'Group Type?' })).toHaveValue('Family'); + console.log('✅ Group Type: Family'); + + await expect(page.getByRole('textbox', { name: 'Address' })).toHaveValue('Sample Address'); + console.log('✅ Address: Sample Address'); + + await expect(page.getByRole('textbox', { name: 'Email?' })).toHaveValue('Doefamiliy@mail.com'); + console.log('✅ Email: Doefamiliy@mail.com'); + + await expect(page.getByRole('link', { name: '09987654321' })).toBeVisible(); + console.log('✅ Phone: 09987654321'); + + console.log('✅ All fields verified — group registrant saved successfully'); + + await page.getByRole('button', { name: 'Browse All (Audit)' }).click(); + await page.getByRole('menuitem', { name: 'All Groups' }).click(); + await page.waitForLoadState('domcontentloaded'); + await expect(page.getByRole('cell', { name: 'Doe Family' })).toBeVisible({ timeout: 10_000 }); + console.log('✅ Group "Doe Family" verified in Browse All Groups list'); + + await page.getByRole('cell', { name: 'Doe Family' }).click(); + await page.waitForLoadState('domcontentloaded'); + await expect(page.locator('.o_form_view')).toBeVisible({ timeout: 10_000 }); + await expect(page.getByRole('textbox', { name: 'Group Name?' })).toHaveValue('Doe Family'); + console.log('✅ Group registrant "Doe Family" created via GUI successfully'); + }); + + test('08 - create group registrant: Santos Family', async () => { + await page.getByRole('menuitem', { name: 'Registry' }).nth(1).click(); + await page.waitForLoadState('domcontentloaded'); + await page.getByRole('button', { name: 'New Group' }).click(); + await page.waitForLoadState('domcontentloaded'); + + await page.getByRole('textbox', { name: 'Group Name?' }).fill('Santos Family'); + await page.getByRole('combobox', { name: 'Group Type?' }).click(); + await page.getByRole('option', { name: 'Family' }).click(); + console.log('✅ Group name and type filled'); + + await page.getByRole('textbox', { name: 'Address' }).fill('456 Rizal Street'); + console.log('✅ Address filled'); + + await page.getByRole('textbox', { name: 'Email?' }).fill('santosfamily@mail.com'); + await page.getByRole('button', { name: 'Add a line' }).first().click(); + await page.locator('input[type="tel"]').fill('09112233445'); + await page.getByRole('row', { name: 'Delete row' }).getByRole('combobox').fill('ph'); + await page.getByRole('option', { name: 'Philippines' }).click(); + console.log('✅ Contact info filled'); + + await page.getByRole('button', { name: 'Save manually' }).click(); + await expect(page.locator('.o_form_view')).toBeVisible(); + console.log('✅ Group registrant saved — verifying all fields...'); + + await expect(page.getByRole('textbox', { name: 'Group Name?' })).toHaveValue('Santos Family'); + console.log('✅ Group Name: Santos Family'); + + await expect(page.getByRole('combobox', { name: 'Group Type?' })).toHaveValue('Family'); + console.log('✅ Group Type: Family'); + + await expect(page.getByRole('textbox', { name: 'Address' })).toHaveValue('456 Rizal Street'); + console.log('✅ Address: 456 Rizal Street'); + + await expect(page.getByRole('textbox', { name: 'Email?' })).toHaveValue('santosfamily@mail.com'); + console.log('✅ Email: santosfamily@mail.com'); + + await expect(page.getByRole('link', { name: '09112233445' })).toBeVisible(); + console.log('✅ Phone: 09112233445'); + + console.log('✅ All fields verified — group registrant saved successfully'); + + await page.getByRole('button', { name: 'Browse All (Audit)' }).click(); + await page.getByRole('menuitem', { name: 'All Groups' }).click(); + await page.waitForLoadState('domcontentloaded'); + await expect(page.getByRole('cell', { name: 'Santos Family' })).toBeVisible({ timeout: 10_000 }); + console.log('✅ Group "Santos Family" verified in Browse All Groups list'); + + await page.getByRole('cell', { name: 'Santos Family' }).click(); + await page.waitForLoadState('domcontentloaded'); + await expect(page.locator('.o_form_view')).toBeVisible({ timeout: 10_000 }); + await expect(page.getByRole('textbox', { name: 'Group Name?' })).toHaveValue('Santos Family'); + console.log('✅ Group registrant "Santos Family" created via GUI successfully'); + }); + + test('09 - assign individuals to groups', async () => { + // --- Doe Family: assign first 3 individuals --- + await page.getByRole('button', { name: 'Browse All (Audit)' }).click(); + await page.getByRole('menuitem', { name: 'All Groups' }).click(); + await page.waitForLoadState('domcontentloaded'); + await page.getByRole('cell', { name: 'Doe Family' }).click(); + await page.waitForLoadState('domcontentloaded'); + await page.getByRole('tab', { name: 'Participation' }).click(); + + await page.getByRole('button', { name: 'Add a line' }).click(); + await page.getByRole('combobox').first().click(); + await page.getByRole('option', { name: 'DOE, JOHN MILLER' }).click(); + console.log('✅ Added DOE, JOHN MILLER to Doe Family'); + + await page.getByRole('button', { name: 'Add a line' }).click(); + await page.getByRole('combobox').first().click(); + await page.getByRole('option', { name: 'REYES, MARIA CLARA' }).click(); + console.log('✅ Added REYES, MARIA CLARA to Doe Family'); + + await page.getByRole('button', { name: 'Add a line' }).click(); + await page.getByRole('combobox').first().click(); + await page.getByRole('option', { name: 'SANTOS, JOSE MIGUEL' }).click(); + console.log('✅ Added SANTOS, JOSE MIGUEL to Doe Family'); + + await page.getByRole('button', { name: 'Save manually' }).click(); + await expect(page.locator('.o_form_view')).toBeVisible(); + console.log('✅ Doe Family members saved — verifying...'); + + await page.getByRole('tab', { name: 'Participation' }).click(); + await expect(page.getByRole('cell', { name: 'DOE, JOHN MILLER' })).toBeVisible({ timeout: 10_000 }); + await expect(page.getByRole('cell', { name: 'REYES, MARIA CLARA' })).toBeVisible({ timeout: 10_000 }); + await expect(page.getByRole('cell', { name: 'SANTOS, JOSE MIGUEL' })).toBeVisible({ timeout: 10_000 }); + console.log('✅ All 3 members verified in Doe Family participation tab'); + + // --- Santos Family: assign last 2 individuals --- + await page.getByRole('button', { name: 'Browse All (Audit)' }).click(); + await page.getByRole('menuitem', { name: 'All Groups' }).click(); + await page.waitForLoadState('domcontentloaded'); + await page.getByRole('cell', { name: 'Santos Family' }).click(); + await page.waitForLoadState('domcontentloaded'); + await page.getByRole('tab', { name: 'Participation' }).click(); + + await page.getByRole('button', { name: 'Add a line' }).click(); + await page.getByRole('combobox').first().click(); + await page.getByRole('option', { name: 'GARCIA, ANA LIZA' }).click(); + console.log('✅ Added GARCIA, ANA LIZA to Santos Family'); + + await page.getByRole('button', { name: 'Add a line' }).click(); + await page.getByRole('combobox').first().click(); + await page.getByRole('option', { name: 'CRUZ, ROBERTO PAOLO' }).click(); + console.log('✅ Added CRUZ, ROBERTO PAOLO to Santos Family'); + + await page.getByRole('button', { name: 'Save manually' }).click(); + await expect(page.locator('.o_form_view')).toBeVisible(); + console.log('✅ Santos Family members saved — verifying...'); + + await page.getByRole('tab', { name: 'Participation' }).click(); + await expect(page.getByRole('cell', { name: 'GARCIA, ANA LIZA' })).toBeVisible({ timeout: 10_000 }); + await expect(page.getByRole('cell', { name: 'CRUZ, ROBERTO PAOLO' })).toBeVisible({ timeout: 10_000 }); + console.log('✅ All 2 members verified in Santos Family participation tab'); + }); + + test('10 - create areas', async () => { + await page.getByRole('menuitem', { name: 'Area' }).click(); + await page.waitForLoadState('domcontentloaded'); + + // --- Philippines (top-level) --- + await page.getByRole('button', { name: 'New' }).click(); + await page.waitForLoadState('domcontentloaded'); + await page.getByRole('textbox', { name: 'Enter Area Name...' }).fill('Philippines'); + await page.getByRole('textbox', { name: 'Code' }).fill('PH01'); + await page.getByRole('textbox', { name: 'Alternate Names' }).fill('ph'); + await page.getByRole('combobox', { name: 'Area Type?' }).click(); + await page.getByRole('option', { name: 'Admin Area' }).click(); + await page.getByRole('textbox', { name: 'Area (km²)?' }).fill('100000'); + await page.getByRole('button', { name: 'Save manually' }).click(); + await expect(page.getByRole('textbox', { name: 'Enter Area Name...' })).toHaveValue('Philippines'); + console.log('✅ Area created: Philippines (PH01)'); + + // --- Manila (under Philippines) --- + await page.getByRole('button', { name: 'New' }).click(); + await page.waitForLoadState('domcontentloaded'); + await page.getByRole('combobox', { name: 'Parent' }).click(); + await page.getByRole('option', { name: 'Philippines (PH01)' }).click(); + await page.getByRole('textbox', { name: 'Enter Area Name...' }).fill('Manila'); + await page.getByRole('textbox', { name: 'Code' }).fill('MNL001'); + await page.getByRole('textbox', { name: 'Alternate Names' }).fill('MNL'); + await page.getByRole('combobox', { name: 'Area Type?' }).click(); + await page.getByRole('option', { name: 'Admin Area' }).click(); + await page.getByRole('textbox', { name: 'Area (km²)?' }).fill('5000'); + await page.getByRole('button', { name: 'Save manually' }).click(); + await expect(page.getByRole('textbox', { name: 'Enter Area Name...' })).toHaveValue('Manila'); + console.log('✅ Area created: Manila (MNL001) under Philippines'); + + // --- Cebu (under Philippines) --- + await page.getByRole('button', { name: 'New' }).click(); + await page.waitForLoadState('domcontentloaded'); + await page.getByRole('combobox', { name: 'Parent' }).click(); + await page.getByRole('option', { name: 'Philippines (PH01)' }).click(); + await page.getByRole('textbox', { name: 'Enter Area Name...' }).fill('Cebu'); + await page.getByRole('textbox', { name: 'Code' }).fill('CEB007'); + await page.getByRole('textbox', { name: 'Alternate Names' }).fill('CEB'); + await page.getByRole('combobox', { name: 'Area Type?' }).click(); + await page.getByRole('option', { name: 'Admin Area' }).click(); + await page.getByRole('textbox', { name: 'Area (km²)?' }).fill('5000'); + await page.getByRole('button', { name: 'Save manually' }).click(); + await expect(page.getByRole('textbox', { name: 'Enter Area Name...' })).toHaveValue('Cebu'); + console.log('✅ Area created: Cebu (CEB007) under Philippines'); + + // --- Mandaue City (under Cebu) --- + await page.getByRole('button', { name: 'New' }).click(); + await page.waitForLoadState('domcontentloaded'); + await page.getByRole('combobox', { name: 'Parent' }).click(); + await page.getByRole('option', { name: 'Cebu (CEB007)' }).click(); + await page.getByRole('textbox', { name: 'Enter Area Name...' }).fill('Mandaue City'); + await page.getByRole('textbox', { name: 'Code' }).fill('MC004'); + await page.getByRole('textbox', { name: 'Alternate Names' }).fill('MC'); + await page.getByRole('combobox', { name: 'Area Type?' }).click(); + await page.getByRole('option', { name: 'Admin Area' }).click(); + await page.getByRole('textbox', { name: 'Area (km²)?' }).fill('500'); + await page.getByRole('button', { name: 'Save manually' }).click(); + await expect(page.getByRole('textbox', { name: 'Enter Area Name...' })).toHaveValue('Mandaue City'); + console.log('✅ Area created: Mandaue City (MC004) under Cebu'); + }); + + test('11 - assign areas to individuals', async () => { + const assignments: Array<{ name: string; area: string }> = [ + { name: 'CRUZ, ROBERTO PAOLO', area: 'Philippines (PH01)' }, + { name: 'GARCIA, ANA LIZA', area: 'Manila (MNL001)' }, + { name: 'SANTOS, JOSE MIGUEL', area: 'Cebu (CEB007)' }, + { name: 'REYES, MARIA CLARA', area: 'Mandaue City (MC004)' }, + { name: 'DOE, JOHN MILLER', area: 'Cebu (CEB007)' }, + ]; + + await page.getByRole('list').getByRole('menuitem', { name: 'Registry' }).click(); + await page.getByRole('button', { name: 'Browse All (Audit)' }).click(); + await page.getByRole('menuitem', { name: 'All Individuals' }).click(); + await page.waitForLoadState('domcontentloaded'); + + for (const { name, area } of assignments) { + await page.getByRole('cell', { name }).click(); + await page.waitForLoadState('domcontentloaded'); + await page.getByPlaceholder('Area').click(); + await page.getByPlaceholder('Area').press('ArrowDown'); + await page.getByRole('option', { name: area }).click(); + await page.getByRole('button', { name: 'Save manually' }).click(); + await expect(page.getByPlaceholder('Area')).toHaveValue(area, { timeout: 10_000 }); + console.log(`✅ Area assigned and verified: ${name} → ${area}`); + await page.getByRole('link', { name: 'Browse All Individuals' }).click(); + await page.waitForLoadState('domcontentloaded'); + } + + console.log('✅ All individuals assigned to areas successfully'); + }); + + test('12 - assign areas to groups', async () => { + const assignments: Array<{ name: string; area: string }> = [ + { name: 'Doe Family', area: 'Philippines (PH01)' }, + { name: 'Santos Family', area: 'Cebu (CEB007)' }, + ]; + + await page.getByRole('button', { name: 'Browse All (Audit)' }).click(); + await page.getByRole('menuitem', { name: 'All Groups' }).click(); + await page.waitForLoadState('domcontentloaded'); + + for (const { name, area } of assignments) { + await page.getByRole('cell', { name }).click(); + await page.waitForLoadState('domcontentloaded'); + await page.getByPlaceholder('Area').click(); + await page.getByPlaceholder('Area').press('ArrowDown'); + await page.getByRole('option', { name: area }).click(); + await page.getByRole('button', { name: 'Save manually' }).click(); + await expect(page.getByPlaceholder('Area')).toHaveValue(area, { timeout: 10_000 }); + console.log(`✅ Area assigned and verified: ${name} → ${area}`); + await page.getByRole('link', { name: 'Browse All Groups' }).click(); + await page.waitForLoadState('domcontentloaded'); + } + + console.log('✅ All groups assigned to areas successfully'); + }); + + test('13 - configure approval definition for change requests', async () => { + // --- Create Approval Definition: "Change request" (Model: Change Request, Group: Change Requests / Validator HQ) --- + await page.getByRole('list').getByRole('menuitem', { name: 'Approvals' }).click(); + await page.getByRole('button', { name: 'Configuration' }).click(); + await page.getByRole('menuitem', { name: 'Approval Definitions' }).click(); + await page.waitForLoadState('domcontentloaded'); + await page.getByRole('button', { name: 'New' }).click(); + await page.waitForLoadState('domcontentloaded'); + + await page.getByRole('textbox', { name: 'Name' }).fill('Change request'); + await page.getByRole('combobox', { name: 'Model' }).click(); + await page.getByRole('combobox', { name: 'Model' }).fill('change'); + await page.getByRole('option', { name: 'Change Request', exact: true }).click(); + await page.getByRole('combobox', { name: 'Approval Group' }).click(); + await page.getByRole('option', { name: 'Change Requests / Validator HQ' }).click(); + console.log('✅ Approval Definition filled: Change request'); + + await page.getByRole('button', { name: 'Save manually' }).click(); + await expect(page.getByRole('textbox', { name: 'Name' })).toHaveValue('Change request'); + console.log('✅ Approval Definition saved: Change request (Model: Change Request, Group: Change Requests / Validator HQ)'); + + // --- Assign the Approval Definition as the Approval Workflow for the basic change request types --- + await page.getByRole('list').getByRole('menuitem', { name: 'Approvals' }).click(); + await page.getByRole('menuitem', { name: 'Change Requests' }).click(); + await page.getByRole('button', { name: 'Configuration' }).click(); + await page.getByRole('menuitem', { name: 'Change Request Types' }).click(); + await page.waitForLoadState('domcontentloaded'); + + const changeRequestTypes = ['Edit Individual Information', 'Edit Group Information', 'Update ID Document']; + + for (const typeName of changeRequestTypes) { + await page.getByRole('cell', { name: typeName }).click(); + await page.waitForLoadState('domcontentloaded'); + await page.getByRole('tab', { name: 'Approval' }).click(); + await page.getByRole('combobox', { name: 'Approval Workflow' }).click(); + await page.getByRole('combobox', { name: 'Approval Workflow' }).press('ArrowDown'); + await page.getByRole('option', { name: 'Change request' }).click(); + + await page.getByRole('button', { name: 'Save manually' }).click(); + await expect(page.getByRole('combobox', { name: 'Approval Workflow' })).toHaveValue('Change request'); + console.log(`✅ Approval Workflow "Change request" (HQ validator) assigned to ${typeName}`); + + await page.getByRole('link', { name: 'Change Request Types' }).click(); + await page.waitForLoadState('domcontentloaded'); + } + + console.log('✅ Approval Workflow assigned to all basic change request types'); + }); + + test('14 - define CR document types for the Philippines (manual vocabulary codes)', async () => { + // Mirrors spp_mis_demo_v2's MisDemoGenerator.CR_DOCUMENT_TYPES["phl"] list, added by hand + // to the existing "Change Request Document Types" vocabulary (shipped by spp_vocabulary, + // is_system=False) to prove the manual GUI flow works without the demo data generator. + const documentTypes = [ + { code: 'psa_birth_certificate', display: 'PSA Birth Certificate' }, + { code: 'philsys_id', display: 'PhilSys National ID' }, + { code: 'barangay_residency_certificate', display: 'Barangay Certificate of Residency' }, + { code: 'psa_marriage_certificate', display: 'PSA Marriage Certificate' }, + { code: 'proof_of_income', display: 'Proof of Income' }, + ]; + + await page.getByRole('menuitem', { name: 'Settings' }).click(); + await page.waitForLoadState('domcontentloaded'); + await page.getByRole('button', { name: 'Vocabularies' }).click(); + await page.getByRole('menuitem', { name: 'Manage Vocabularies' }).click(); + await page.waitForLoadState('domcontentloaded'); + await page.getByRole('button', { name: 'Remove' }).click(); + await page.getByRole('searchbox', { name: 'Search...' }).fill('change'); + await page.getByRole('searchbox', { name: 'Search...' }).press('Enter'); + await page.waitForLoadState('domcontentloaded'); + await page.getByRole('cell', { name: 'Change Request Document Types' }).click(); + await page.waitForLoadState('domcontentloaded'); + await page.getByRole('tab', { name: 'Codes' }).click(); + + for (const { code, display } of documentTypes) { + await page.getByRole('button', { name: 'Add a line' }).click(); + const row = page.locator('.o_data_row').last(); + await row.getByRole('textbox').first().fill(code); + await row.getByRole('textbox').nth(1).fill(display); + console.log(`✅ Vocabulary code row filled: ${display} (${code})`); + } + + await page.getByRole('button', { name: 'Save manually' }).click(); + for (const { display } of documentTypes) { + await expect(page.getByRole('cell', { name: display })).toBeVisible({ timeout: 10_000 }); + } + console.log('✅ All 5 CR document types saved under the Change Request Document Types vocabulary'); + }); + + test('15 - define additional CR document types for the Philippines (BIR 2316, Academic Calendar, Authorization Letter, Certificate of Enrolment, Valid ID of Parent)', async () => { + // Matches the e2e fixture PDFs in e2e/fixtures/, added to the same "Change Request Document Types" + // vocabulary as test 14, ahead of a later test that uploads these files via the Update ID Document + // change request flow. Test 14 leaves the page sitting on this same record's Codes tab after saving, + // so no re-navigation is needed here (and re-clicking "Settings" from inside Settings is ambiguous). + const documentTypes = [ + { code: 'bir_2316', display: 'BIR Form 2316' }, + { code: 'academic_calendar', display: 'Academic Calendar' }, + { code: 'authorization_letter', display: 'Authorization Letter' }, + { code: 'certificate_of_enrolment', display: 'Certificate of Enrolment' }, + { code: 'valid_id_parent', display: 'Valid ID of Parent' }, + ]; + + for (const { code, display } of documentTypes) { + await page.getByRole('button', { name: 'Add a line' }).click(); + const row = page.locator('.o_data_row').last(); + await row.getByRole('textbox').first().fill(code); + await row.getByRole('textbox').nth(1).fill(display); + console.log(`✅ Vocabulary code row filled: ${display} (${code})`); + } + + await page.getByRole('button', { name: 'Save manually' }).click(); + for (const { display } of documentTypes) { + await expect(page.getByRole('cell', { name: display })).toBeVisible({ timeout: 10_000 }); + } + console.log('✅ All 5 additional CR document types saved under the Change Request Document Types vocabulary'); + }); +}); diff --git a/e2e/tests/02-spp-starter-farmer-registry.spec.ts b/e2e/tests/02-spp-starter-farmer-registry.spec.ts new file mode 100644 index 00000000..1aa4f178 --- /dev/null +++ b/e2e/tests/02-spp-starter-farmer-registry.spec.ts @@ -0,0 +1,62 @@ +import { test, expect, Page } from '@playwright/test'; +import { resetStack } from './helpers'; + +async function login(page: Page) { + await page.goto('/web/login'); + await page.getByRole('textbox', { name: 'Email' }).fill('admin'); + await page.getByRole('textbox', { name: 'Password' }).fill('admin'); + await page.getByRole('button', { name: 'Log in' }).click(); + await expect(page.locator('.o_main_navbar')).toBeVisible({ timeout: 30_000 }); +} + +async function installApp(page: Page, technicalName: string) { + console.log('✅ Clicking Apps menuitem'); + await page.getByRole('menuitem', { name: 'Apps' }).click(); + await page.waitForLoadState('domcontentloaded'); + console.log('✅ Apps page loaded'); + + await page.getByRole('button', { name: 'Remove' }).click(); + console.log('✅ Clicked Remove button'); + await page.waitForLoadState('domcontentloaded'); + console.log('✅ Filter cleared, page settled'); + + console.log('✅ Filling search box'); + await page.getByRole('searchbox', { name: 'Search...' }).fill(technicalName); + await page.getByRole('searchbox', { name: 'Search...' }).press('Enter'); + await page.waitForLoadState('domcontentloaded'); + console.log('✅ Search done, looking for Install button'); + + const installBtn = page.getByRole('button', { name: 'Activate' }).first(); + await expect(installBtn).toBeVisible({ timeout: 15_000 }); + console.log('✅ Install button found, clicking'); + await installBtn.click(); + + console.log('✅ Waiting for installation to complete'); + await page.waitForURL(/^(?!.*action-178).*/, { timeout: 180_000 }); + await page.waitForLoadState('domcontentloaded', { timeout: 180_000 }); + await page.locator('.o_loading').waitFor({ state: 'hidden', timeout: 180_000 }).catch(() => {}); + await expect(page.locator('.o_main_navbar .o_menu_sections .o_nav_entry').first()).toBeVisible({ timeout: 180_000 }); + console.log('✅ Installation complete — nav menus are visible'); +} + +test.describe('OpenSPP', () => { + test.beforeAll(async () => { + await resetStack(); + }); + + test('login and install OpenSPP Starter Farmer Registry, then verify nav menus', async ({ page }) => { + await login(page); + + await installApp(page, 'spp_starter_farmer_registry'); + + // Wait 1 minute for menus to fully settle after installation + await page.waitForTimeout(60_000); + + // Verify navbar and menus are present + await expect(page.locator('.o_main_navbar')).toBeVisible(); + const menuItems = page.locator('.o_main_navbar .o_nav_entry, .o_main_navbar .o_dropdown'); + await expect(menuItems.first()).toBeVisible({ timeout: 10_000 }); + + await page.screenshot({ path: 'reports/post-install-farmer-registry-navbar.png', fullPage: false }); + }); +}); diff --git a/e2e/tests/helpers.ts b/e2e/tests/helpers.ts new file mode 100644 index 00000000..af99a787 --- /dev/null +++ b/e2e/tests/helpers.ts @@ -0,0 +1,28 @@ +import { execSync } from 'child_process'; + +const COMPOSE_CWD = '/home/penn_ubuntu/OpenSPP2'; +const HEALTH_URL = 'http://localhost:8069/web/health'; + +export async function resetStack() { + console.log('🔄 Tearing down stack and volumes...'); + execSync('docker compose --profile ui down -v', { stdio: 'inherit', cwd: COMPOSE_CWD }); + + console.log('🚀 Starting fresh stack...'); + execSync('docker compose --profile ui up -d --build', { stdio: 'inherit', cwd: COMPOSE_CWD }); + + console.log('⏳ Waiting for Odoo to be healthy...'); + await waitForHealth(); + console.log('✅ Odoo is ready'); +} + +async function waitForHealth(retries = 30, intervalMs = 10_000) { + for (let i = 1; i <= retries; i++) { + try { + const res = await fetch(HEALTH_URL); + if (res.ok) return; + } catch {} + console.log(` Health check ${i}/${retries} — retrying in ${intervalMs / 1000}s`); + await new Promise(r => setTimeout(r, intervalMs)); + } + throw new Error('Odoo did not become healthy in time'); +} diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json new file mode 100644 index 00000000..6902913e --- /dev/null +++ b/e2e/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "CommonJS", + "strict": true, + "types": ["node", "@playwright/test"] + } +}