diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 67e9928..49a858f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,34 +2,16 @@ name: Lint modules run-name: ${{ inputs.module && format('Lint {0}', inputs.module) || ' ' }} on: - pull_request: - branches: [ master ] - paths-ignore: - - "**/README.md" push: paths-ignore: - "**/README.md" workflow_dispatch: inputs: - module: - description: 'Module to lint (e.g., common-styling, user, chat)' - required: true - type: choice - options: - - chat - - common-styling - - core - - data-export-api - - oauth-facebook - - oauth-github - - oauth-google - - openai - - payments - - payments-example-gateway - - payments-stripe - - reports - - tests - - user + modules: + description: 'Modules to lint (comma-separated or "all" for all modules)' + required: false + default: 'all' + type: string jobs: pre_job: @@ -42,7 +24,7 @@ jobs: with: github_token: ${{ github.token }} paths_ignore: '["**/README.md"]' - do_not_skip: '["push"]' + do_not_skip: '["push", "workflow_dispatch"]' detect-changes: needs: pre_job @@ -55,7 +37,10 @@ jobs: - uses: dorny/paths-filter@v3 id: filter + if: github.event_name != 'workflow_dispatch' with: + base: ${{ github.event.before }} + ref: ${{ github.event.after }} filters: | payments: - 'pos-module-payments/**' @@ -89,173 +74,228 @@ jobs: - name: Set matrix for changed modules id: set-matrix run: | - # Extract module names where filter output is "true" - modules=$(echo '${{ toJSON(steps.filter.outputs) }}' | jq -c '[to_entries[] | select(.value == "true") | .key]') + # Check if this is a manual trigger + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "Manual trigger detected" + manual_input="${{ github.event.inputs.modules }}" + echo "Manual input: $manual_input" + + if [ "$manual_input" = "all" ] || [ -z "$manual_input" ]; then + # Dynamically discover all modules + modules=$(ls -d pos-module-*/ | sed 's|pos-module-||g; s|/||g' | jq -R -s -c 'split("\n") | map(select(length > 0))') + else + # Parse comma-separated list, strip pos-module- prefix, and create array + modules=$(echo "$manual_input" | jq -R -c 'split(",") | map(gsub("^\\s+|\\s+$"; "") | gsub("^pos-module-"; ""))') + fi + echo "Manual trigger modules: $modules" + else + # Automatic detection from git changes + changes='${{ steps.filter.outputs.changes }}' + echo "Detected changes: $changes" + modules="$changes" + echo "Changed modules for linting: $modules" + fi echo "matrix=$modules" >> $GITHUB_OUTPUT - echo "Changed modules for linting: $modules" run-linter: - needs: [pre_job, detect-changes] - if: | - always() && - needs.pre_job.outputs.should_skip != 'true' && - (github.event_name == 'workflow_dispatch' || - needs.detect-changes.outputs.changed-modules != '[]') - runs-on: ubuntu-latest - strategy: - matrix: - module: ${{ github.event_name == 'workflow_dispatch' && fromJSON(format('["{0}"]', inputs.module)) || fromJSON(needs.detect-changes.outputs.changed-modules) }} - fail-fast: false - env: - CI: true - LOGS_DIR: ${{ github.workspace }}/pos-module-${{ matrix.module }}/logs - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Node.js - uses: actions/setup-node@v3 - with: - node-version: '22' + needs: [pre_job, detect-changes] + if: | + needs.pre_job.outputs.should_skip != 'true' && + needs.detect-changes.result == 'success' && + needs.detect-changes.outputs.changed-modules != '[]' + runs-on: ubuntu-latest + strategy: + matrix: + module: ${{ fromJSON(needs.detect-changes.outputs.changed-modules) }} + fail-fast: false + env: + CI: true + LOGS_DIR: ${{ github.workspace }}/pos-module-${{ matrix.module }}/logs + steps: + - name: Checkout code + uses: actions/checkout@v4 - - name: Install pos-cli - id: install_pos_cli - run: npm install -g @platformos/pos-cli@latest + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '22' - - name: Set up logs directory - if: steps.install_pos_cli.outcome == 'success' - run: | - mkdir -p ${{ env.LOGS_DIR }} - chmod -R 777 ${{ env.LOGS_DIR }} + - name: Install pos-cli + id: install_pos_cli + run: npm install -g @platformos/pos-cli@latest - # - name: Install modules - # if: steps.install_pos_cli.outcome == 'success' - # run: | - # cd pos-module-${{ matrix.module }} - # mkdir -p app - # pos-cli modules install + - name: Set up logs directory + if: steps.install_pos_cli.outcome == 'success' + run: | + mkdir -p ${{ env.LOGS_DIR }} + chmod -R 777 ${{ env.LOGS_DIR }} - - name: Run pos-cli check - if: steps.install_pos_cli.outcome == 'success' - id: run_check - run: | - set +e # Disable exit on error - pos-cli check run pos-module-${{ matrix.module }} -f json > ${{ env.LOGS_DIR }}/platformos-check-raw.json - check_exit_code=$? # Capture the exit code - set -e # Re-enable exit on error + # - name: Install modules + # if: steps.install_pos_cli.outcome == 'success' + # run: | + # cd pos-module-${{ matrix.module }} + # mkdir -p app + # pos-cli modules install - jq . ${{ env.LOGS_DIR }}/platformos-check-raw.json | tee ${{ env.LOGS_DIR }}/platformos-check.json + - name: Run pos-cli check + if: steps.install_pos_cli.outcome == 'success' + id: run_check + run: | + set +e # Disable exit on error + pos-cli check run pos-module-${{ matrix.module }} -f json > ${{ env.LOGS_DIR }}/platformos-check-raw.json + check_exit_code=$? # Capture the exit code + set -e # Re-enable exit on error - exit $check_exit_code + jq . ${{ env.LOGS_DIR }}/platformos-check-raw.json | tee ${{ env.LOGS_DIR }}/platformos-check.json - - name: Upload logs - if: always() && steps.run_check.outcome != 'skipped' - uses: actions/upload-artifact@v4 - with: - name: platformos_check_logs_${{ matrix.module }}_${{ github.run_id }} - path: | - ${{ env.LOGS_DIR }}/platformos-check.json - ${{ env.LOGS_DIR }}/platformos-check-raw.json - - - name: Generate summary - if: always() && steps.run_check.outcome != 'skipped' - run: | - echo "# PlatformOS Check Summary - ${{ matrix.module }} :checkered_flag:" >> $GITHUB_STEP_SUMMARY - echo "## Result: ${{ steps.run_check.outcome }} ${{ env.RESULT_ICON }}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY + exit $check_exit_code - if [ -f "${{ env.LOGS_DIR }}/platformos-check.json" ]; then - JSON="${{ env.LOGS_DIR }}/platformos-check.json" + - name: Upload logs + if: always() && steps.run_check.outcome != 'skipped' + uses: actions/upload-artifact@v4 + with: + name: platformos_check_logs_${{ matrix.module }}_${{ github.run_id }} + path: | + ${{ env.LOGS_DIR }}/platformos-check.json + ${{ env.LOGS_DIR }}/platformos-check-raw.json - # Summary statistics - echo "## Summary" >> $GITHUB_STEP_SUMMARY - echo "| Metric | Count |" >> $GITHUB_STEP_SUMMARY - echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY - jq -r '"| Total Offenses | \(.offenseCount) |", "| Files with Issues | \(.fileCount) |", "| Errors | \(.errorCount) |", "| Warnings | \(.warningCount) |", "| Info | \(.infoCount) |"' "$JSON" >> $GITHUB_STEP_SUMMARY + - name: Generate summary + if: always() && steps.run_check.outcome != 'skipped' + run: | + echo "# PlatformOS Check Summary - ${{ matrix.module }} :checkered_flag:" >> $GITHUB_STEP_SUMMARY + echo "## Result: ${{ steps.run_check.outcome }} ${{ env.RESULT_ICON }}" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - # Offenses by check type - echo "## Offenses by Check Type" >> $GITHUB_STEP_SUMMARY - echo "| Check | Total | Errors | Warnings |" >> $GITHUB_STEP_SUMMARY - echo "|-------|-------|--------|----------|" >> $GITHUB_STEP_SUMMARY - jq -r ' - [.files[].offenses[]] | group_by(.check) - | map({ - check: .[0].check, - total: length, - errors: map(select(.severity == "error")) | length, - warnings: map(select(.severity == "warning")) | length - }) - | sort_by(-.total) - | .[] - | "| `\(.check)` | \(.total) | \(.errors) | \(.warnings) |" - ' "$JSON" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY + if [ -f "${{ env.LOGS_DIR }}/platformos-check.json" ]; then + JSON="${{ env.LOGS_DIR }}/platformos-check.json" - # Errors section — full detail for files with errors - ERROR_FILE_COUNT=$(jq '[.files[] | select(.errorCount > 0)] | length' "$JSON") - if [ "$ERROR_FILE_COUNT" -gt 0 ]; then - echo "## Errors ($ERROR_FILE_COUNT files)" >> $GITHUB_STEP_SUMMARY + # Summary statistics + echo "## Summary" >> $GITHUB_STEP_SUMMARY + echo "| Metric | Count |" >> $GITHUB_STEP_SUMMARY + echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY + jq -r '"| Total Offenses | \(.offenseCount) |", "| Files with Issues | \(.fileCount) |", "| Errors | \(.errorCount) |", "| Warnings | \(.warningCount) |", "| Info | \(.infoCount) |"' "$JSON" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY + + # Offenses by check type + echo "## Offenses by Check Type" >> $GITHUB_STEP_SUMMARY + echo "| Check | Total | Errors | Warnings |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|--------|----------|" >> $GITHUB_STEP_SUMMARY jq -r ' - [.files[] | select(.errorCount > 0)] - | sort_by(-.errorCount) + [.files[].offenses[]] | group_by(.check) + | map({ + check: .[0].check, + total: length, + errors: map(select(.severity == "error")) | length, + warnings: map(select(.severity == "warning")) | length + }) + | sort_by(-.total) | .[] - | "### \u274c `\(.path)`", - "", - "**Errors:** \(.errorCount) | **Warnings:** \(.warningCount)", - "", - "| Line | Col | Severity | Check | Message |", - "|------|-----|----------|-------|---------|", - (.offenses | sort_by( - (if .severity == "error" then 0 elif .severity == "warning" then 1 else 2 end), - .start_row - ) | .[] | "| \(.start_row) | \(.start_column) | \(.severity) | `\(.check)` | \(.message | gsub("\n"; " ")) |"), - "" + | "| `\(.check)` | \(.total) | \(.errors) | \(.warnings) |" ' "$JSON" >> $GITHUB_STEP_SUMMARY - fi - - # Warnings section — collapsed, only top files shown - WARNING_ONLY_COUNT=$(jq '[.files[] | select(.errorCount == 0 and .warningCount > 0)] | length' "$JSON") - if [ "$WARNING_ONLY_COUNT" -gt 0 ]; then - TOTAL_WARNINGS=$(jq '.warningCount' "$JSON") - echo "
" >> $GITHUB_STEP_SUMMARY - echo "## Warnings — ${TOTAL_WARNINGS} across ${WARNING_ONLY_COUNT} files (click to expand)" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - # Show top 20 files by warning count - jq -r ' - [.files[] | select(.errorCount == 0 and .warningCount > 0)] - | sort_by(-.warningCount) - | .[:20] - | .[] - | "### \u26a0\ufe0f `\(.path)` — \(.warningCount) warnings", - "", - "| Line | Col | Check | Message |", - "|------|-----|-------|---------|", - (.offenses | sort_by(.start_row) - | .[] | "| \(.start_row) | \(.start_column) | `\(.check)` | \(.message | gsub("\n"; " ")) |"), - "" - ' "$JSON" >> $GITHUB_STEP_SUMMARY + # Errors section — full detail for files with errors + ERROR_FILE_COUNT=$(jq '[.files[] | select(.errorCount > 0)] | length' "$JSON") + if [ "$ERROR_FILE_COUNT" -gt 0 ]; then + echo "## Errors ($ERROR_FILE_COUNT files)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + jq -r ' + [.files[] | select(.errorCount > 0)] + | sort_by(-.errorCount) + | .[] + | "### \u274c `\(.path)`", + "", + "**Errors:** \(.errorCount) | **Warnings:** \(.warningCount)", + "", + "| Line | Col | Severity | Check | Message |", + "|------|-----|----------|-------|---------|", + (.offenses | sort_by( + (if .severity == "error" then 0 elif .severity == "warning" then 1 else 2 end), + .start_row + ) | .[] | "| \(.start_row) | \(.start_column) | \(.severity) | `\(.check)` | \(.message | gsub("\n"; " ")) |"), + "" + ' "$JSON" >> $GITHUB_STEP_SUMMARY + fi - REMAINING=$((WARNING_ONLY_COUNT - 20)) - if [ "$REMAINING" -gt 0 ]; then - echo "...and ${REMAINING} more files with warnings only." >> $GITHUB_STEP_SUMMARY + # Warnings section — collapsed, only top files shown + WARNING_ONLY_COUNT=$(jq '[.files[] | select(.errorCount == 0 and .warningCount > 0)] | length' "$JSON") + if [ "$WARNING_ONLY_COUNT" -gt 0 ]; then + TOTAL_WARNINGS=$(jq '.warningCount' "$JSON") + echo "
" >> $GITHUB_STEP_SUMMARY + echo "## Warnings — ${TOTAL_WARNINGS} across ${WARNING_ONLY_COUNT} files (click to expand)" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY + + # Show top 20 files by warning count + jq -r ' + [.files[] | select(.errorCount == 0 and .warningCount > 0)] + | sort_by(-.warningCount) + | .[:20] + | .[] + | "### \u26a0\ufe0f `\(.path)` — \(.warningCount) warnings", + "", + "| Line | Col | Check | Message |", + "|------|-----|-------|---------|", + (.offenses | sort_by(.start_row) + | .[] | "| \(.start_row) | \(.start_column) | `\(.check)` | \(.message | gsub("\n"; " ")) |"), + "" + ' "$JSON" >> $GITHUB_STEP_SUMMARY + + REMAINING=$((WARNING_ONLY_COUNT - 20)) + if [ "$REMAINING" -gt 0 ]; then + echo "...and ${REMAINING} more files with warnings only." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + fi + + echo "
" >> $GITHUB_STEP_SUMMARY fi + else + echo "## No output file found" >> $GITHUB_STEP_SUMMARY + echo "Expected location: ${{ env.LOGS_DIR }}/platformos-check.json" >> $GITHUB_STEP_SUMMARY + fi + env: + RESULT_ICON: ${{ steps.run_check.outcome == 'success' && ':white_check_mark:' || ':x:' }} - echo "
" >> $GITHUB_STEP_SUMMARY + - name: Fail job if platformos-check failed + if: always() && steps.run_check.outcome == 'failure' + run: | + echo "platformos-check failed — marking job as failed" + exit 1 + + conclusion: + needs: [detect-changes, run-linter] + if: always() + runs-on: ubuntu-latest + steps: + - name: Generate workflow summary + run: | + if [ "${{ needs.detect-changes.outputs.changed-modules }}" = "[]" ] || [ "${{ needs.detect-changes.result }}" = "skipped" ]; then + echo "## Linting - Skipped" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${{ needs.detect-changes.result }}" = "skipped" ]; then + echo "Workflow was skipped by duplicate action check." >> $GITHUB_STEP_SUMMARY + elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "Manual trigger with no matching modules selected." >> $GITHUB_STEP_SUMMARY + else + echo "No modules were changed in this push." >> $GITHUB_STEP_SUMMARY fi else - echo "## No output file found" >> $GITHUB_STEP_SUMMARY - echo "Expected location: ${{ env.LOGS_DIR }}/platformos-check.json" >> $GITHUB_STEP_SUMMARY + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "## Linting - Completed (Manual Trigger)" >> $GITHUB_STEP_SUMMARY + else + echo "## Linting - Completed" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + echo "Linting ran for the following modules:" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo '${{ needs.detect-changes.outputs.changed-modules }}' | jq -r '.[] | "- " + .' >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${{ needs.run-linter.result }}" = "success" ]; then + echo "Result: All checks passed" >> $GITHUB_STEP_SUMMARY + elif [ "${{ needs.run-linter.result }}" = "skipped" ]; then + echo "Result: Linting was skipped" >> $GITHUB_STEP_SUMMARY + else + echo "Result: Some checks failed - see job output for details" >> $GITHUB_STEP_SUMMARY + fi fi - env: - RESULT_ICON: ${{ steps.run_check.outcome == 'success' && ':white_check_mark:' || ':x:' }} - - - name: Fail job if platformos-check failed - if: always() && steps.run_check.outcome == 'failure' - run: | - echo "platformos-check failed — marking job as failed" - exit 1 diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index ffa02f5..2a4ddc9 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -1,13 +1,15 @@ name: E2E tests on: - pull_request: - branches: [ master ] - paths-ignore: - - "**/README.md" push: paths-ignore: - "**/README.md" workflow_dispatch: + inputs: + modules: + description: 'Modules to test (comma-separated: user, chat, common-styling, payments-example-gateway, or "all")' + required: false + default: 'all' + type: string jobs: pre_job: @@ -20,7 +22,7 @@ jobs: with: github_token: ${{ github.token }} paths_ignore: '["**/README.md"]' - do_not_skip: '["push"]' + do_not_skip: '["push", "workflow_dispatch"]' detect-changes: needs: pre_job @@ -33,7 +35,10 @@ jobs: - uses: dorny/paths-filter@v3 id: filter + if: github.event_name != 'workflow_dispatch' with: + base: ${{ github.event.before }} + ref: ${{ github.event.after }} filters: | user: - 'pos-module-user/**' @@ -41,6 +46,8 @@ jobs: - 'pos-module-chat/**' common-styling: - 'pos-module-common-styling/**' + payments-example-gateway: + - 'pos-module-payments-example-gateway/**' - name: Set matrix for changed modules id: set-matrix @@ -65,21 +72,51 @@ jobs: "path": "pos-module-common-styling", "deploy-script": "pos-cli data clean --include-schema --auto-confirm\npos-cli deploy", "test-commands": "npm run pw-tests" + }, + "payments-example-gateway": { + "module": "payments-example-gateway", + "path": "pos-module-payments-example-gateway", + "deploy-script": "./tests/data/seed/seed.sh", + "test-commands": "npm run pw-tests" } } EOF - # Extract changed modules and map to their configurations - modules=$(echo '${{ toJSON(steps.filter.outputs) }}' | \ - jq -c --slurpfile config /tmp/module-config.json \ - '[to_entries[] | select(.value == "true") | .key as $m | $config[0][$m] | select(. != null)]') + # Check if this is a manual trigger + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "Manual trigger detected" + manual_input="${{ github.event.inputs.modules }}" + echo "Manual input: $manual_input" + + if [ "$manual_input" = "all" ] || [ -z "$manual_input" ]; then + # Test all modules + modules=$(jq -c '[.user, .chat, ."common-styling", ."payments-example-gateway"]' /tmp/module-config.json) + else + # Parse comma-separated list, strip pos-module- prefix, and map to configurations + modules=$(echo "$manual_input" | \ + jq -R -c 'split(",") | map(gsub("^\\s+|\\s+$"; "") | gsub("^pos-module-"; ""))' | \ + jq -c --slurpfile config /tmp/module-config.json \ + 'map(. as $m | $config[0][$m] | select(. != null))') + fi + echo "Manual trigger modules: $modules" + else + # Automatic detection from git changes + changes='${{ steps.filter.outputs.changes }}' + echo "Detected changes: $changes" + + # Extract changed modules and map to their configurations + modules=$(jq -nc --argjson changes "$changes" --slurpfile config /tmp/module-config.json \ + '$changes | map(. as $m | $config[0][$m] | select(. != null))') + echo "Changed modules matrix: $modules" + fi echo "matrix=$modules" >> $GITHUB_OUTPUT - echo "Changed modules matrix: $modules" test-e2e: needs: detect-changes - if: needs.detect-changes.outputs.changed-modules != '[]' + if: | + needs.detect-changes.result == 'success' && + needs.detect-changes.outputs.changed-modules != '[]' runs-on: ubuntu-latest container: ${{ vars.PW_CONTAINER }} strategy: @@ -148,3 +185,41 @@ jobs: method: release repository-url: ${{ vars.CI_PS_REPOSITORY_URL }} pos-ci-repo-token: ${{ secrets.POS_CI_PS_REPO_ACCESS_TOKEN }} + + conclusion: + needs: [detect-changes, test-e2e] + if: always() + runs-on: ubuntu-latest + steps: + - name: Generate workflow summary + run: | + if [ "${{ needs.detect-changes.outputs.changed-modules }}" = "[]" ] || [ "${{ needs.detect-changes.result }}" = "skipped" ]; then + echo "## E2E Tests - Skipped" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${{ needs.detect-changes.result }}" = "skipped" ]; then + echo "Workflow was skipped by duplicate action check." >> $GITHUB_STEP_SUMMARY + elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "Manual trigger with no matching modules selected." >> $GITHUB_STEP_SUMMARY + else + echo "No modules with E2E tests were changed in this push." >> $GITHUB_STEP_SUMMARY + fi + else + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "## E2E Tests - Completed (Manual Trigger)" >> $GITHUB_STEP_SUMMARY + else + echo "## E2E Tests - Completed" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + echo "Tests ran for the following modules:" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo '${{ needs.detect-changes.outputs.changed-modules }}' | jq -r '.[] | "- " + .module' >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${{ needs.test-e2e.result }}" = "success" ]; then + echo "Result: All tests passed" >> $GITHUB_STEP_SUMMARY + elif [ "${{ needs.test-e2e.result }}" = "skipped" ]; then + echo "Result: Tests were skipped" >> $GITHUB_STEP_SUMMARY + else + echo "Result: Some tests failed - check job output for details" >> $GITHUB_STEP_SUMMARY + fi + fi diff --git a/pos-module-chat/package-lock.json b/pos-module-chat/package-lock.json index e619fa1..d29dfe6 100644 --- a/pos-module-chat/package-lock.json +++ b/pos-module-chat/package-lock.json @@ -5,17 +5,18 @@ "packages": { "": { "devDependencies": { - "@playwright/test": "^1.49.1", + "@playwright/test": "^1.58.2", "@types/node": "^22.10.6" } }, "node_modules/@playwright/test": { - "version": "1.49.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz", - "integrity": "sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==", + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "playwright": "1.49.1" + "playwright": "1.58.2" }, "bin": { "playwright": "cli.js" @@ -39,6 +40,7 @@ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -48,12 +50,13 @@ } }, "node_modules/playwright": { - "version": "1.49.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz", - "integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==", + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.49.1" + "playwright-core": "1.58.2" }, "bin": { "playwright": "cli.js" @@ -66,10 +69,11 @@ } }, "node_modules/playwright-core": { - "version": "1.49.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz", - "integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==", + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", "dev": true, + "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" }, diff --git a/pos-module-common-styling/package-lock.json b/pos-module-common-styling/package-lock.json index 2d6e70f..5bbf3e7 100644 --- a/pos-module-common-styling/package-lock.json +++ b/pos-module-common-styling/package-lock.json @@ -22,6 +22,7 @@ "rollup-plugin-postcss": "^4.0.2" }, "devDependencies": { + "@playwright/test": "^1.58.2", "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.3", @@ -162,6 +163,22 @@ "@lezer/highlight": "^1.0.0" } }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -2126,6 +2143,38 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", diff --git a/pos-module-payments-example-gateway/README.md b/pos-module-payments-example-gateway/README.md index 6ed34c0..b0030ee 100644 --- a/pos-module-payments-example-gateway/README.md +++ b/pos-module-payments-example-gateway/README.md @@ -35,6 +35,29 @@ EXAMPLE GATEWAY ``` +## Testing + +This module includes E2E tests using Playwright. + +### Running Tests + +```bash +npm run pw-tests +``` + +### Test Coverage + +The test suite includes: +- Payment page loading +- Successful payment flows +- Failed payment flows +- Delayed payment processing +- Invalid transaction handling +- Multiple payment attempts +- URL parameter preservation + +For more details, see [Test README](tests/README.md). + ## TODO - [ ] do the page that similate external api diff --git a/pos-module-payments-example-gateway/package-lock.json b/pos-module-payments-example-gateway/package-lock.json new file mode 100644 index 0000000..0addaf3 --- /dev/null +++ b/pos-module-payments-example-gateway/package-lock.json @@ -0,0 +1,91 @@ +{ + "name": "pos-module-payments-example-gateway", + "version": "0.1.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pos-module-payments-example-gateway", + "version": "0.1.1", + "hasInstallScript": true, + "devDependencies": { + "@playwright/test": "^1.58.2", + "@types/node": "^22.0.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "dev": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "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 + } + } +} diff --git a/pos-module-payments-example-gateway/package.json b/pos-module-payments-example-gateway/package.json new file mode 100644 index 0000000..4b25311 --- /dev/null +++ b/pos-module-payments-example-gateway/package.json @@ -0,0 +1,13 @@ +{ + "name": "pos-module-payments-example-gateway", + "version": "0.1.1", + "private": true, + "scripts": { + "postinstall": "npx playwright install chromium", + "pw-tests": "playwright test tests --project=smoke-tests" + }, + "devDependencies": { + "@playwright/test": "^1.58.2", + "@types/node": "^22.0.0" + } +} diff --git a/pos-module-payments-example-gateway/playwright.config.ts b/pos-module-payments-example-gateway/playwright.config.ts new file mode 100644 index 0000000..044b36e --- /dev/null +++ b/pos-module-payments-example-gateway/playwright.config.ts @@ -0,0 +1,26 @@ +import { defineConfig, devices } from '@playwright/test'; +import process from 'process'; + +export default defineConfig({ + testDir: './tests', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 3 : 3, + reporter: [ + ['list'], + ['html', { outputFolder: 'playwright-report', open: 'never' }], + ], + use: { + baseURL: process.env.MPKIT_URL, + screenshot: { mode: 'only-on-failure', fullPage: true }, + trace: 'retain-on-failure', + }, + projects: [ + { + name: 'smoke-tests', + testMatch: /.*\.spec\.ts/, + use: { ...devices['Desktop Chrome'] }, + }, + ], +}); diff --git a/pos-module-payments-example-gateway/tests/README.md b/pos-module-payments-example-gateway/tests/README.md new file mode 100644 index 0000000..818222c --- /dev/null +++ b/pos-module-payments-example-gateway/tests/README.md @@ -0,0 +1,20 @@ +# E2E Tests for Payment Gateway Module + +This directory contains end-to-end tests for the `payments_example_gateway` module using Playwright. + +## Running Tests + +```bash +npm run pw-tests +``` + +## Test Coverage + +- Payment page loads successfully +- Successful payment flow end-to-end +- Failed payment flow end-to-end +- Delayed payment success flow +- Invalid transaction handling (404) +- Missing transaction_id handling (404) +- Multiple payment attempts on same transaction +- URL parameters preservation in redirect flow diff --git a/pos-module-payments-example-gateway/tests/data/seed/seed.sh b/pos-module-payments-example-gateway/tests/data/seed/seed.sh new file mode 100755 index 0000000..2d0acf8 --- /dev/null +++ b/pos-module-payments-example-gateway/tests/data/seed/seed.sh @@ -0,0 +1,11 @@ +set -eu + +DEFAULT_ENV="" +POS_ENV="${1:-$DEFAULT_ENV}" + +mkdir -p app/ +# This also installs core module +pos-cli modules install payments + +pos-cli data clean $POS_ENV --auto-confirm --include-schema +pos-cli deploy $POS_ENV diff --git a/pos-module-payments-example-gateway/tests/invalid-transaction.spec.ts b/pos-module-payments-example-gateway/tests/invalid-transaction.spec.ts new file mode 100644 index 0000000..b3b8d3d --- /dev/null +++ b/pos-module-payments-example-gateway/tests/invalid-transaction.spec.ts @@ -0,0 +1,25 @@ +// spec: tests/payment-gateway-smoke.plan.md +// seed: tests/seed.spec.ts + +import { test, expect } from '@playwright/test'; + +test.describe('Payment Gateway Smoke Tests', () => { + test('Invalid transaction handling', async ({ page }) => { + // 1. Navigate directly to payment gateway page with invalid transaction_id + + // expect: Navigate to /payments/example_gateway/index?transaction_id=invalid-id-12345 + const response = await page.goto('/payments/example_gateway?transaction_id=invalid-id-12345'); + + // 2. Verify error handling + + // expect: Page returns 404 status code + expect(response?.status()).toBe(404); + + // expect: Transaction query returns blank/null + // expect: Payment gateway page does not render + // expect: Proper error handling prevents payment processing with invalid transaction + + // Verify that the payment form is not rendered + await expect(page.locator('form[action*="/payments/example_gateway/webhook"]')).not.toBeVisible(); + }); +}); diff --git a/pos-module-payments-example-gateway/tests/missing-transaction-id.spec.ts b/pos-module-payments-example-gateway/tests/missing-transaction-id.spec.ts new file mode 100644 index 0000000..633f263 --- /dev/null +++ b/pos-module-payments-example-gateway/tests/missing-transaction-id.spec.ts @@ -0,0 +1,22 @@ +// spec: tests/payment-gateway-smoke.plan.md +// seed: tests/seed.spec.ts + +import { test, expect } from '@playwright/test'; + +test.describe('Payment Gateway Smoke Tests', () => { + test('Payment gateway page without transaction_id', async ({ page }) => { + // 1. Navigate to payment gateway page without transaction_id parameter + + // expect: Navigate to /payments/example_gateway/index (no parameters) + const response = await page.goto('/payments/example_gateway'); + + // 2. Verify error handling + + // expect: Page returns 404 status code or appropriate error + expect(response?.status()).toBe(404); + + // expect: Transaction cannot be found without transaction_id + // expect: Payment form does not render without valid transaction + await expect(page.locator('form[action*="/payments/example_gateway/webhook"]')).not.toBeVisible(); + }); +}); diff --git a/pos-module-payments-example-gateway/tests/multiple-payment-attempts.spec.ts b/pos-module-payments-example-gateway/tests/multiple-payment-attempts.spec.ts new file mode 100644 index 0000000..0754d00 --- /dev/null +++ b/pos-module-payments-example-gateway/tests/multiple-payment-attempts.spec.ts @@ -0,0 +1,47 @@ +// spec: tests/payment-gateway-smoke.plan.md +// seed: tests/seed.spec.ts + +import { test, expect } from '@playwright/test'; + +test.describe('Payment Gateway Smoke Tests', () => { + test('Multiple payment attempts on same transaction', async ({ page }) => { + // 1. Create a test transaction and complete successful payment + await page.goto('/test-payment'); + + // expect: Transaction is created + await page.locator('#start-payment').click(); + await page.waitForURL(/\/payments\/example_gateway/); + + // Capture the transaction ID from the URL + const gatewayUrl = page.url(); + const transactionIdMatch = gatewayUrl.match(/transaction_id=([^&]+)/); + expect(transactionIdMatch).not.toBeNull(); + const transactionId = transactionIdMatch![1]; + + // expect: Payment succeeds + const successButton = page.getByRole('button', { name: /Payment Success/i }).first(); + await successButton.click(); + + // expect: Transaction status is 'succeeded' + await page.waitForURL(/\/test-payment\?payment_success=1/); + await expect(page.getByText(/Payment Successful/i)).toBeVisible(); + + // 2. Attempt to access the same transaction's payment gateway page again + + // expect: Navigate back to the gateway URL with the same transaction_id + await page.goto(gatewayUrl); + + // 3. Verify transaction state handling + + // expect: Gateway page may load or show appropriate message for already-completed transaction + // expect: System handles duplicate payment attempts gracefully + // expect: Transaction status remains 'succeeded' and is not changed + + // The page should either: + // 1. Show an error message about the transaction being already processed, or + // 2. Still show the payment form but not change the transaction status + + // We'll check if the page loads (doesn't throw an error) + expect(page.url()).toContain('transaction_id=' + transactionId); + }); +}); diff --git a/pos-module-payments-example-gateway/tests/payment-failed-flow.spec.ts b/pos-module-payments-example-gateway/tests/payment-failed-flow.spec.ts new file mode 100644 index 0000000..bcaa93d --- /dev/null +++ b/pos-module-payments-example-gateway/tests/payment-failed-flow.spec.ts @@ -0,0 +1,48 @@ +// spec: tests/payment-gateway-smoke.plan.md +// seed: tests/seed.spec.ts + +import { test, expect } from '@playwright/test'; + +test.describe('Payment Gateway Smoke Tests', () => { + test('Failed payment flow end-to-end', async ({ page }) => { + // 1. Navigate to /test-payment page + await page.goto('/test-payment'); + + // expect: Page loads successfully with the test payment form + await expect(page.getByRole('heading', { name: /Test Payment/i })).toBeVisible(); + + // 2. Click the 'Start Test Payment' button (id='start-payment') + await page.locator('#start-payment').click(); + + // expect: Form submits and creates a new transaction + // expect: User is redirected to payment gateway page at /payments/example_gateway/index + await page.waitForURL(/\/payments\/example_gateway/); + + // expect: Gateway page loads with transaction details and payment options + await expect(page.getByRole('heading', { name: /Example Payment Gateway/i })).toBeVisible(); + + // 3. Click 'Payment Failed' button + const failedButton = page.getByRole('button', { name: /Payment Failed/i }); + await failedButton.click(); + + // expect: Form submits to webhook endpoint with payment_status=failed + // expect: Webhook processes the failed payment status + // expect: Transaction status is updated to 'failed' + // expect: User is redirected to the failed_url: /test-payment?payment_failed=1 + await page.waitForURL(/\/test-payment\?payment_failed=1/); + + // 4. Verify failure page displays correctly + + // expect: User lands on /test-payment page with payment_failed=1 parameter + expect(page.url()).toMatch(/payment_failed=1/); + + // expect: Red error message is displayed: 'Payment Failed' + await expect(page.getByText(/Payment Failed/i)).toBeVisible(); + + // expect: Error message includes text: 'Your test payment was not processed.' + await expect(page.getByText(/Your test payment was not processed/i)).toBeVisible(); + + // expect: Start Test Payment button is available to retry + await expect(page.locator('#start-payment')).toBeVisible(); + }); +}); diff --git a/pos-module-payments-example-gateway/tests/payment-gateway-smoke.plan.md b/pos-module-payments-example-gateway/tests/payment-gateway-smoke.plan.md new file mode 100644 index 0000000..6a004dc --- /dev/null +++ b/pos-module-payments-example-gateway/tests/payment-gateway-smoke.plan.md @@ -0,0 +1,161 @@ +# Payment Example Gateway - Smoke Tests + +## Application Overview + +The Payment Example Gateway module provides a mock payment gateway for testing platformOS payment flows. It includes a test helper page at /test-payment that creates test transactions and a gateway page that simulates payment processing with success/failure options. The payment flow is: test-payment page → payment gateway page → success/failure redirect. This test plan covers the core smoke tests to verify the payment gateway integration is functioning correctly. + +## Test Scenarios + +### 1. Payment Gateway Smoke Tests + +**Seed:** `tests/seed.spec.ts` + +#### 1.1. Test payment page loads successfully + +**File:** `tests/payment-page-load.spec.ts` + +**Steps:** + 1. Navigate to /test-payment page + - expect: Page loads with status 200 + - expect: Page displays the heading 'Test Payment - Example Gateway' + - expect: Page shows test transaction details: Amount: $10.99, Currency: USD, Items: test-item-1, test-item-2 + - expect: Page displays the 'Start Test Payment' button with id='start-payment' + - expect: Info message is visible explaining this is a test payment gateway + 2. Verify page structure and content + - expect: Transaction details are visible showing: Amount: $10.99, Currency: USD, Gateway: example_gateway + - expect: Form element with POST method to /test-payment?create=1 is present + - expect: Submit button with id 'start-payment' is present and clickable + - expect: E2E testing instructions are visible at the bottom of the page + +#### 1.2. Successful payment flow end-to-end + +**File:** `tests/payment-success-flow.spec.ts` + +**Steps:** + 1. Navigate to /test-payment page + - expect: Page loads successfully with the test payment form + 2. Click the 'Start Test Payment' button (id='start-payment') + - expect: Form submits and creates a new transaction + - expect: User is redirected to /payments/example_gateway/index page + - expect: URL includes transaction_id parameter + - expect: URL includes success_url parameter set to /test-payment?payment_success=1 + - expect: URL includes failed_url parameter set to /test-payment?payment_failed=1 + 3. Verify payment gateway page loads + - expect: Page displays heading 'Example Payment Gateway' + - expect: Page shows 'Select payment status:' text + - expect: Three buttons are visible: 'Payment Success', 'Payment Failed', and 'Payment Success delay status change for 15s' + - expect: Payment Success button shows the transaction amount: $10.99 + - expect: Form action points to /payments/example_gateway/webhook + - expect: Hidden input fields contain transaction_id, success_url, and failed_url + 4. Click 'Payment Success' button + - expect: Form submits to webhook endpoint + - expect: Webhook processes the payment_status=success + - expect: Transaction status is updated to 'succeeded' + - expect: User is redirected to the success_url: /test-payment?payment_success=1 + 5. Verify success page displays correctly + - expect: User lands on /test-payment page with payment_success=1 parameter + - expect: Green success message is displayed: 'Payment Successful!' + - expect: Success message includes text: 'Your test payment was processed successfully.' + - expect: Start Test Payment button is still available for additional tests + +#### 1.3. Failed payment flow end-to-end + +**File:** `tests/payment-failed-flow.spec.ts` + +**Steps:** + 1. Navigate to /test-payment page + - expect: Page loads successfully with the test payment form + 2. Click the 'Start Test Payment' button (id='start-payment') + - expect: Form submits and creates a new transaction + - expect: User is redirected to payment gateway page at /payments/example_gateway/index + - expect: Gateway page loads with transaction details and payment options + 3. Click 'Payment Failed' button + - expect: Form submits to webhook endpoint with payment_status=failed + - expect: Webhook processes the failed payment status + - expect: Transaction status is updated to 'failed' + - expect: User is redirected to the failed_url: /test-payment?payment_failed=1 + 4. Verify failure page displays correctly + - expect: User lands on /test-payment page with payment_failed=1 parameter + - expect: Red error message is displayed: 'Payment Failed' + - expect: Error message includes text: 'Your test payment was not processed.' + - expect: Start Test Payment button is available to retry + +#### 1.4. Delayed payment success flow + +**File:** `tests/payment-success-delayed.spec.ts` + +**Steps:** + 1. Navigate to /test-payment page + - expect: Page loads successfully + 2. Click the 'Start Test Payment' button + - expect: User is redirected to payment gateway page + 3. Verify delayed payment button is present + - expect: Third button with text 'Payment Success delay status change for 15s' is visible + - expect: Button shows transaction amount: $10.99 + - expect: Button has name='payment_status' and value='success_delayed' + 4. Click 'Payment Success delay status change for 15s' button + - expect: Form submits to webhook endpoint with payment_status=success_delayed + - expect: Webhook queues background job to update transaction status after 15 second delay + - expect: User is immediately redirected to success_url: /test-payment?payment_success=1 + - expect: Success page displays while transaction processes in background + 5. Verify success page displays + - expect: Green success message is displayed + - expect: Transaction will be updated to 'succeeded' status after background job completes (15 seconds) + +#### 1.5. Invalid transaction handling + +**File:** `tests/invalid-transaction.spec.ts` + +**Steps:** + 1. Navigate directly to payment gateway page with invalid transaction_id + - expect: Navigate to /payments/example_gateway/index?transaction_id=invalid-id-12345 + 2. Verify error handling + - expect: Page returns 404 status code + - expect: Transaction query returns blank/null + - expect: Payment gateway page does not render + - expect: Proper error handling prevents payment processing with invalid transaction + +#### 1.6. Payment gateway page without transaction_id + +**File:** `tests/missing-transaction-id.spec.ts` + +**Steps:** + 1. Navigate to payment gateway page without transaction_id parameter + - expect: Navigate to /payments/example_gateway/index (no parameters) + 2. Verify error handling + - expect: Page returns 404 status code or appropriate error + - expect: Transaction cannot be found without transaction_id + - expect: Payment form does not render without valid transaction + +#### 1.7. Multiple payment attempts on same transaction + +**File:** `tests/multiple-payment-attempts.spec.ts` + +**Steps:** + 1. Create a test transaction and complete successful payment + - expect: Transaction is created + - expect: Payment succeeds + - expect: Transaction status is 'succeeded' + 2. Attempt to access the same transaction's payment gateway page again + - expect: Navigate back to the gateway URL with the same transaction_id + 3. Verify transaction state handling + - expect: Gateway page may load or show appropriate message for already-completed transaction + - expect: System handles duplicate payment attempts gracefully + - expect: Transaction status remains 'succeeded' and is not changed + +#### 1.8. URL parameters preservation in redirect flow + +**File:** `tests/url-parameters-preservation.spec.ts` + +**Steps:** + 1. Create transaction and navigate to payment gateway page + - expect: Gateway page loads with transaction_id, success_url, and failed_url parameters + 2. Verify all required URL parameters are present + - expect: transaction_id is present in URL + - expect: success_url parameter equals /test-payment?payment_success=1 + - expect: failed_url parameter equals /test-payment?payment_failed=1 (note: code shows 'failed_url' but test-payment.liquid uses 'cancel_url') + - expect: Form hidden inputs contain all three parameters for webhook submission + 3. Submit payment and verify redirect URL + - expect: After clicking Payment Success, user is redirected to exact success_url + - expect: After clicking Payment Failed, user is redirected to exact failed_url + - expect: No parameters are lost during redirect chain diff --git a/pos-module-payments-example-gateway/tests/payment-page-load.spec.ts b/pos-module-payments-example-gateway/tests/payment-page-load.spec.ts new file mode 100644 index 0000000..285ce45 --- /dev/null +++ b/pos-module-payments-example-gateway/tests/payment-page-load.spec.ts @@ -0,0 +1,48 @@ +// spec: tests/payment-gateway-smoke.plan.md +// seed: tests/seed.spec.ts + +import { test, expect } from '@playwright/test'; + +test.describe('Payment Gateway Smoke Tests', () => { + test('Test payment page loads successfully', async ({ page }) => { + // 1. Navigate to /test-payment page + await page.goto('/test-payment'); + + // expect: Page loads with status 200 + expect(page).toHaveURL(/\/test-payment/); + + // expect: Page displays the heading 'Test Payment - Example Gateway' + await expect(page.getByRole('heading', { name: /Test Payment - Example Gateway/i })).toBeVisible(); + + // expect: Page shows test transaction details: Amount: $10.99, Currency: USD, Items: test-item-1, test-item-2 + await expect(page.getByText('$10.99')).toBeVisible(); + await expect(page.getByText('USD')).toBeVisible(); + await expect(page.getByText(/test-item-1/)).toBeVisible(); + await expect(page.getByText(/test-item-2/)).toBeVisible(); + + // expect: Page displays the 'Start Test Payment' button with id='start-payment' + await expect(page.locator('#start-payment')).toBeVisible(); + + // expect: Info message is visible explaining this is a test payment gateway + await expect(page.getByText(/test payment gateway/i)).toBeVisible(); + + // 2. Verify page structure and content + + // expect: Transaction details are visible showing: Amount: $10.99, Currency: USD, Gateway: example_gateway + await expect(page.getByText(/Amount/i)).toBeVisible(); + await expect(page.getByText(/Currency/i)).toBeVisible(); + await expect(page.getByText(/example_gateway/i)).toBeVisible(); + + // expect: Form element with POST method to /test-payment?create=1 is present + const form = page.locator('form[action*="/test-payment"]'); + await expect(form).toBeVisible(); + + // expect: Submit button with id 'start-payment' is present and clickable + const submitButton = page.locator('#start-payment'); + await expect(submitButton).toBeVisible(); + await expect(submitButton).toBeEnabled(); + + // expect: E2E testing instructions are visible at the bottom of the page + await expect(page.getByText(/E2E/i)).toBeVisible(); + }); +}); diff --git a/pos-module-payments-example-gateway/tests/payment-success-delayed.spec.ts b/pos-module-payments-example-gateway/tests/payment-success-delayed.spec.ts new file mode 100644 index 0000000..f246c6c --- /dev/null +++ b/pos-module-payments-example-gateway/tests/payment-success-delayed.spec.ts @@ -0,0 +1,53 @@ +// spec: tests/payment-gateway-smoke.plan.md +// seed: tests/seed.spec.ts + +import { test, expect } from '@playwright/test'; + +test.describe('Payment Gateway Smoke Tests', () => { + test('Delayed payment success flow', async ({ page }) => { + // 1. Navigate to /test-payment page + await page.goto('/test-payment'); + + // expect: Page loads successfully + await expect(page.getByRole('heading', { name: /Test Payment/i })).toBeVisible(); + + // 2. Click the 'Start Test Payment' button + await page.locator('#start-payment').click(); + + // expect: User is redirected to payment gateway page + await page.waitForURL(/\/payments\/example_gateway/); + + // 3. Verify delayed payment button is present + + // expect: Third button with text 'Payment Success delay status change for 15s' is visible + // Note: The button contains HTML tags, so we match on the button value attribute + const delayedButton = page.locator('button[name="payment_status"][value="success_delayed"]'); + await expect(delayedButton).toBeVisible(); + + // expect: Button has name='payment_status' and value='success_delayed' + await expect(delayedButton).toHaveAttribute('name', 'payment_status'); + await expect(delayedButton).toHaveAttribute('value', 'success_delayed'); + + // expect: Button shows transaction amount: $10.99 + await expect(delayedButton).toContainText('$10.99'); + + // 4. Click 'Payment Success delay status change for 15s' button + await delayedButton.click(); + + // expect: Form submits to webhook endpoint with payment_status=success_delayed + // expect: Webhook queues background job to update transaction status after 15 second delay + // expect: User is immediately redirected to success_url: /test-payment?payment_success=1 + await page.waitForURL(/\/test-payment\?payment_success=1/); + + // expect: Success page displays while transaction processes in background + expect(page.url()).toMatch(/payment_success=1/); + + // 5. Verify success page displays + + // expect: Green success message is displayed + await expect(page.getByText(/Payment Successful/i)).toBeVisible(); + + // expect: Transaction will be updated to 'succeeded' status after background job completes (15 seconds) + // Note: We don't wait for the background job to complete in this test + }); +}); diff --git a/pos-module-payments-example-gateway/tests/payment-success-flow.spec.ts b/pos-module-payments-example-gateway/tests/payment-success-flow.spec.ts new file mode 100644 index 0000000..e04f713 --- /dev/null +++ b/pos-module-payments-example-gateway/tests/payment-success-flow.spec.ts @@ -0,0 +1,82 @@ +// spec: tests/payment-gateway-smoke.plan.md +// seed: tests/seed.spec.ts + +import { test, expect } from '@playwright/test'; + +test.describe('Payment Gateway Smoke Tests', () => { + test('Successful payment flow end-to-end', async ({ page }) => { + // 1. Navigate to /test-payment page + await page.goto('/test-payment'); + + // expect: Page loads successfully with the test payment form + await expect(page.getByRole('heading', { name: /Test Payment/i })).toBeVisible(); + + // 2. Click the 'Start Test Payment' button (id='start-payment') + await page.locator('#start-payment').click(); + + // expect: Form submits and creates a new transaction + // expect: User is redirected to /payments/example_gateway/index page + await page.waitForURL(/\/payments\/example_gateway/); + + // expect: URL includes transaction_id parameter + expect(page.url()).toMatch(/transaction_id=/); + + // expect: URL includes success_url parameter set to /test-payment?payment_success=1 (URL encoded) + expect(page.url()).toMatch(/success_url=%2Ftest-payment%3Fpayment_success%3D1/); + + // expect: URL includes failed_url parameter set to /test-payment?payment_failed=1 (URL encoded) + expect(page.url()).toMatch(/failed_url=%2Ftest-payment%3Fpayment_failed%3D1/); + + // 3. Verify payment gateway page loads + + // expect: Page displays heading 'Example Payment Gateway' + await expect(page.getByRole('heading', { name: /Example Payment Gateway/i })).toBeVisible(); + + // expect: Page shows 'Select payment status:' text + await expect(page.getByText(/Select payment status/i)).toBeVisible(); + + // expect: Three buttons are visible: 'Payment Success', 'Payment Failed', and 'Payment Success delay status change for 15s' + const successButton = page.getByRole('button', { name: /Payment Success/i }).first(); + const failedButton = page.getByRole('button', { name: /Payment Failed/i }); + const delayedButton = page.getByRole('button', { name: /Payment Success delay/i }); + + await expect(successButton).toBeVisible(); + await expect(failedButton).toBeVisible(); + await expect(delayedButton).toBeVisible(); + + // expect: Payment Success button shows the transaction amount: $10.99 + await expect(successButton).toContainText('$10.99'); + + // expect: Form action points to /payments/example_gateway/webhook + const form = page.locator('form[action*="/payments/example_gateway/webhook"]'); + await expect(form).toBeVisible(); + + // expect: Hidden input fields contain transaction_id, success_url, and failed_url + await expect(page.locator('input[name="transaction_id"]')).toBeAttached(); + await expect(page.locator('input[name="success_url"]')).toBeAttached(); + await expect(page.locator('input[name="failed_url"]')).toBeAttached(); + + // 4. Click 'Payment Success' button + await successButton.click(); + + // expect: Form submits to webhook endpoint + // expect: Webhook processes the payment_status=success + // expect: Transaction status is updated to 'succeeded' + // expect: User is redirected to the success_url: /test-payment?payment_success=1 + await page.waitForURL(/\/test-payment\?payment_success=1/); + + // 5. Verify success page displays correctly + + // expect: User lands on /test-payment page with payment_success=1 parameter + expect(page.url()).toMatch(/payment_success=1/); + + // expect: Green success message is displayed: 'Payment Successful!' + await expect(page.getByText(/Payment Successful/i)).toBeVisible(); + + // expect: Success message includes text: 'Your test payment was processed successfully.' + await expect(page.getByText(/Your test payment was processed successfully/i)).toBeVisible(); + + // expect: Start Test Payment button is still available for additional tests + await expect(page.locator('#start-payment')).toBeVisible(); + }); +}); diff --git a/pos-module-payments-example-gateway/tests/url-parameters-preservation.spec.ts b/pos-module-payments-example-gateway/tests/url-parameters-preservation.spec.ts new file mode 100644 index 0000000..b95ed74 --- /dev/null +++ b/pos-module-payments-example-gateway/tests/url-parameters-preservation.spec.ts @@ -0,0 +1,66 @@ +// spec: tests/payment-gateway-smoke.plan.md +// seed: tests/seed.spec.ts + +import { test, expect } from '@playwright/test'; + +test.describe('Payment Gateway Smoke Tests', () => { + test('URL parameters preservation in redirect flow', async ({ page }) => { + // 1. Create transaction and navigate to payment gateway page + await page.goto('/test-payment'); + await page.locator('#start-payment').click(); + + // expect: Gateway page loads with transaction_id, success_url, and failed_url parameters + await page.waitForURL(/\/payments\/example_gateway/); + + // 2. Verify all required URL parameters are present + + const currentUrl = page.url(); + + // expect: transaction_id is present in URL + expect(currentUrl).toMatch(/transaction_id=[^&]+/); + + // expect: success_url parameter equals /test-payment?payment_success=1 (URL encoded) + expect(currentUrl).toMatch(/success_url=%2Ftest-payment%3Fpayment_success%3D1/); + + // expect: failed_url parameter equals /test-payment?payment_failed=1 (URL encoded) + expect(currentUrl).toMatch(/failed_url=%2Ftest-payment%3Fpayment_failed%3D1/); + + // expect: Form hidden inputs contain all three parameters for webhook submission + const transactionIdInput = page.locator('input[name="transaction_id"]'); + const successUrlInput = page.locator('input[name="success_url"]'); + const failedUrlInput = page.locator('input[name="failed_url"]'); + + await expect(transactionIdInput).toBeAttached(); + await expect(successUrlInput).toBeAttached(); + await expect(failedUrlInput).toBeAttached(); + + // Verify the hidden inputs have values + expect(await transactionIdInput.inputValue()).not.toBe(''); + expect(await successUrlInput.inputValue()).toContain('/test-payment'); + expect(await failedUrlInput.inputValue()).toContain('/test-payment'); + + // 3. Submit payment and verify redirect URL + + // Test success flow + const successButton = page.getByRole('button', { name: /Payment Success/i }).first(); + await successButton.click(); + + // expect: After clicking Payment Success, user is redirected to exact success_url + await page.waitForURL(/\/test-payment\?payment_success=1/); + expect(page.url()).toMatch(/\/test-payment\?payment_success=1/); + + // expect: No parameters are lost during redirect chain + + // Now test failed flow with a new transaction + await page.goto('/test-payment'); + await page.locator('#start-payment').click(); + await page.waitForURL(/\/payments\/example_gateway/); + + const failedButton = page.getByRole('button', { name: /Payment Failed/i }); + await failedButton.click(); + + // expect: After clicking Payment Failed, user is redirected to exact failed_url + await page.waitForURL(/\/test-payment\?payment_failed=1/); + expect(page.url()).toMatch(/\/test-payment\?payment_failed=1/); + }); +}); diff --git a/pos-module-user/package-lock.json b/pos-module-user/package-lock.json index 6f3766d..e597b4a 100644 --- a/pos-module-user/package-lock.json +++ b/pos-module-user/package-lock.json @@ -15,6 +15,7 @@ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "playwright": "1.58.2" }, @@ -40,6 +41,7 @@ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -53,6 +55,7 @@ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", "dev": true, + "license": "Apache-2.0", "dependencies": { "playwright-core": "1.58.2" }, @@ -71,6 +74,7 @@ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", "dev": true, + "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" }, diff --git a/pos-module-user/package.json b/pos-module-user/package.json index eccf068..fe9cc12 100644 --- a/pos-module-user/package.json +++ b/pos-module-user/package.json @@ -8,4 +8,4 @@ "@playwright/test": "^1.58.2", "@types/node": "^22.13.10" } -} \ No newline at end of file +}