diff --git a/.github/workflows/wasm-sdk-ui-tests.yml b/.github/workflows/wasm-sdk-ui-tests.yml new file mode 100644 index 00000000000..238d0f5a1fa --- /dev/null +++ b/.github/workflows/wasm-sdk-ui-tests.yml @@ -0,0 +1,236 @@ +name: WASM SDK UI Automation Tests + +on: + # Trigger after wasm-sdk-build workflow completes successfully + # TODO: Temporarily disabled - uncomment to re-enable automatic triggering + # workflow_run: + # workflows: ["Build WASM SDK"] + # types: + # - completed + # branches: + # - master + # - 'v[0-9]+.[0-9]+-dev' + + # Manual trigger for standalone testing + workflow_dispatch: + inputs: + test_type: + description: 'Type of tests to run' + required: true + default: 'all' + type: choice + options: + - smoke + - queries + - parameterized + - all + browser: + description: 'Browser to use for testing' + required: false + default: 'chromium' + type: choice + options: + - chromium + - firefox + - webkit + headed: + description: 'Run tests in headed mode (visible browser)' + required: false + default: false + type: boolean + debug: + description: 'Enable debug output' + required: false + default: false + type: boolean + workflow_run_id: + description: 'Workflow run ID to download WASM SDK build from (for manual runs)' + required: false + type: string + +jobs: + ui-tests: + name: Run WASM SDK UI Tests + runs-on: ubuntu-latest + timeout-minutes: 20 + # Only run if build workflow succeeded (for workflow_run) or if manually triggered + if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }} + + env: + CI: true + DEBUG: ${{ inputs.debug || 'false' }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Download WASM SDK build artifacts + uses: actions/download-artifact@v4 + with: + # For workflow_run, download from the triggering workflow + # For manual dispatch, use provided workflow_run_id or latest + run-id: ${{ github.event.workflow_run.id || inputs.workflow_run_id }} + name: wasm-sdk-build + path: packages/wasm-sdk/pkg/ + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Verify WASM SDK artifacts + working-directory: packages/wasm-sdk + run: | + echo "Verifying downloaded WASM SDK artifacts..." + ls -lah pkg/ + + # Verify all required files exist + required_files=( + "pkg/wasm_sdk_bg.wasm" + "pkg/optimized.wasm" + "pkg/wasm_sdk.js" + "pkg/wasm_sdk.d.ts" + "pkg/package.json" + ) + + for file in "${required_files[@]}"; do + if [ ! -f "$file" ]; then + echo "โŒ Missing required file: $file" + exit 1 + else + echo "โœ… Found: $file" + fi + done + + echo "๐ŸŽ‰ All WASM SDK artifacts verified successfully!" + + - name: Install test dependencies + working-directory: packages/wasm-sdk/test/ui-automation + run: | + echo "Installing UI test dependencies..." + npm install + + - name: Cache Playwright browsers + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ hashFiles('packages/wasm-sdk/test/ui-automation/package.json') }} + restore-keys: | + ${{ runner.os }}-playwright- + + - name: Install Playwright browsers + working-directory: packages/wasm-sdk/test/ui-automation + run: | + echo "Installing Playwright browsers..." + npx playwright install --with-deps ${{ inputs.browser || 'chromium' }} + + - name: Run smoke tests + if: (inputs.test_type == 'smoke' || inputs.test_type == 'all') || github.event_name == 'workflow_run' + working-directory: packages/wasm-sdk/test/ui-automation + run: | + echo "๐Ÿงช Running smoke tests..." + if [ "${{ inputs.headed }}" == "true" ]; then + npm run test:headed -- tests/basic-smoke.spec.js + else + npm run test:smoke + fi + + - name: Run query execution tests + if: (inputs.test_type == 'queries' || inputs.test_type == 'all') || github.event_name == 'workflow_run' + working-directory: packages/wasm-sdk/test/ui-automation + run: | + echo "๐Ÿ” Running query execution tests..." + if [ "${{ inputs.headed }}" == "true" ]; then + npm run test:headed -- tests/query-execution.spec.js + else + npm run test:queries + fi + + - name: Run parameterized tests + if: (inputs.test_type == 'parameterized' || inputs.test_type == 'all') || github.event_name == 'workflow_run' + working-directory: packages/wasm-sdk/test/ui-automation + run: | + echo "โš™๏ธ Running parameterized tests..." + if [ "${{ inputs.headed }}" == "true" ]; then + npm run test:headed -- tests/parameterized-queries.spec.js + else + npm run test:parameterized + fi + + - name: Upload Playwright Report + uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report-${{ inputs.test_type || 'all' }}-${{ inputs.browser || 'chromium' }}-${{ github.run_number }} + path: packages/wasm-sdk/test/ui-automation/playwright-report/ + retention-days: 30 + + - name: Upload Test Results + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-results-${{ inputs.test_type || 'all' }}-${{ inputs.browser || 'chromium' }}-${{ github.run_number }} + path: | + packages/wasm-sdk/test/ui-automation/test-results/ + packages/wasm-sdk/test/ui-automation/test-results.json + retention-days: 30 + + - name: Upload Screenshots and Videos + uses: actions/upload-artifact@v4 + if: failure() + with: + name: test-failures-${{ inputs.test_type || 'all' }}-${{ inputs.browser || 'chromium' }}-${{ github.run_number }} + path: | + packages/wasm-sdk/test/ui-automation/test-results/**/*.png + packages/wasm-sdk/test/ui-automation/test-results/**/*.webm + retention-days: 30 + + - name: Display Test Summary + if: always() + working-directory: packages/wasm-sdk/test/ui-automation + run: | + echo "## WASM SDK UI Test Results" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + + if [ "${{ github.event_name }}" == "workflow_run" ]; then + echo "- **Triggered by**: WASM SDK Build (workflow_run)" >> "$GITHUB_STEP_SUMMARY" + echo "- **Build Workflow**: ${{ github.event.workflow_run.html_url }}" >> "$GITHUB_STEP_SUMMARY" + else + echo "- **Triggered by**: Manual dispatch" >> "$GITHUB_STEP_SUMMARY" + echo "- **Test Type**: ${{ inputs.test_type }}" >> "$GITHUB_STEP_SUMMARY" + echo "- **Browser**: ${{ inputs.browser }}" >> "$GITHUB_STEP_SUMMARY" + echo "- **Headed Mode**: ${{ inputs.headed }}" >> "$GITHUB_STEP_SUMMARY" + fi + + echo "- **Debug**: ${{ env.DEBUG }}" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + + # Show test results if available + if [ -f "test-results.json" ]; then + echo "### Test Results" >> "$GITHUB_STEP_SUMMARY" + echo '```json' >> "$GITHUB_STEP_SUMMARY" + jq '.stats' test-results.json >> "$GITHUB_STEP_SUMMARY" 2>/dev/null || echo "Test results available in artifacts" >> "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" + fi + + # List available artifacts + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "### Available Artifacts" >> "$GITHUB_STEP_SUMMARY" + echo "- **Playwright Report**: Detailed HTML report with test results" >> "$GITHUB_STEP_SUMMARY" + echo "- **Test Results**: JSON results and raw output files" >> "$GITHUB_STEP_SUMMARY" + if [ "${{ job.status }}" == "failure" ]; then + echo "- **Test Failures**: Screenshots and videos of failed tests" >> "$GITHUB_STEP_SUMMARY" + fi + + # Add quick links + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "### Quick Links" >> "$GITHUB_STEP_SUMMARY" + echo "- [WASM SDK](https://github.com/${{ github.repository }}/tree/${{ github.sha }}/packages/wasm-sdk)" >> "$GITHUB_STEP_SUMMARY" + echo "- [UI Tests](https://github.com/${{ github.repository }}/tree/${{ github.sha }}/packages/wasm-sdk/test/ui-automation)" >> "$GITHUB_STEP_SUMMARY" + \ No newline at end of file diff --git a/packages/wasm-sdk/test/ui-automation/README.md b/packages/wasm-sdk/test/ui-automation/README.md new file mode 100644 index 00000000000..e69ac93ce35 --- /dev/null +++ b/packages/wasm-sdk/test/ui-automation/README.md @@ -0,0 +1,257 @@ +# WASM SDK UI Automation Tests + +Automated testing suite for the WASM SDK web interface (`index.html`) using Playwright. + +## Features + +- **Cross-browser testing** (currently configured for Chromium, easily extensible) +- **Automated parameter injection** from existing test data +- **Page Object Model** for maintainable test code +- **Network switching** (testnet/mainnet) testing +- **Error handling** and validation testing +- **Comprehensive reporting** with screenshots and videos on failure +- **GitHub Actions integration** for automated testing + +## Quick Start + +### Prerequisites + +- Node.js 18+ installed +- Python 3 for serving the web interface +- Linux environment (Ubuntu/Debian recommended) + +### Installation + +```bash +cd /path/to/wasm-sdk/test/ui-automation +npm install +npx playwright install chromium +``` + +### Running Tests + +The easiest way to run tests is using the provided shell script: + +```bash +# From any directory, using the test runner script: +./run-ui-tests.sh smoke # Basic functionality tests +./run-ui-tests.sh queries # Query execution tests +./run-ui-tests.sh parameterized # Comprehensive parameter testing +./run-ui-tests.sh all # Run all tests (default) + +# Run in headed mode (see browser) +./run-ui-tests.sh headed + +# Debug mode with detailed output +DEBUG=true ./run-ui-tests.sh smoke + +# Pattern matching for specific tests +./run-ui-tests.sh --grep="should initialize SDK" +``` + +**Alternative: Direct npm commands** (from ui-automation directory): + +```bash +npm test # Run all tests +npm run test:smoke # Basic functionality tests +npm run test:queries # Query execution tests +npm run test:parameterized # Comprehensive parameter testing +npm run test:headed # Run in headed mode +npm run test:debug # Debug mode +npm run test:report # View HTML report +``` + +## Test Structure + +### Test Categories + +1. **Basic Smoke Tests** (`basic-smoke.spec.js`) + - SDK initialization + - UI component visibility + - Network switching + - Basic interaction flows + +2. **Query Execution Tests** (`query-execution.spec.js`) + - Identity queries (getIdentity, getIdentityBalance, getIdentityKeys, etc.) + - Data contract queries (getDataContract, getDataContracts, getDataContractHistory) + - Document queries (getDocuments, getDocument) + - System queries (getStatus, getCurrentEpoch, getTotalCreditsInPlatform) + - Error handling scenarios + - Proof support testing with automatic fallback + +3. **Parameterized Tests** (`parameterized-queries.spec.js`) + - Multiple parameter sets per query type + - Cross-network testing scenarios + - Parameter validation testing + +### Architecture + +```text +ui-automation/ +โ”œโ”€โ”€ tests/ # Test specification files +โ”‚ โ”œโ”€โ”€ basic-smoke.spec.js # Basic functionality tests +โ”‚ โ”œโ”€โ”€ query-execution.spec.js # Comprehensive query testing +โ”‚ โ””โ”€โ”€ parameterized-queries.spec.js # Multi-parameter testing +โ”œโ”€โ”€ utils/ # Test utilities and page objects +โ”‚ โ”œโ”€โ”€ base-test.js # Base test functionality +โ”‚ โ”œโ”€โ”€ wasm-sdk-page.js # Page Object Model for index.html +โ”‚ โ””โ”€โ”€ parameter-injector.js # Parameter injection system +โ”œโ”€โ”€ fixtures/ # Test data and fixtures +โ”‚ โ””โ”€โ”€ test-data.js # Centralized test parameters +โ”œโ”€โ”€ playwright.config.js # Playwright configuration +โ”œโ”€โ”€ run-ui-tests.sh # Comprehensive test runner script +โ””โ”€โ”€ package.json # npm scripts and dependencies +``` + +## Configuration + +### Playwright Configuration + +The `playwright.config.js` file is configured for: + +- **Base URL**: `http://localhost:8888` (server managed via Playwright config's `webServer`) +- **Browsers**: Chromium (headless by default) +- **Timeouts**: 30s for actions, 120s for tests +- **Reporters**: HTML, JSON, and console output +- **Screenshots/Videos**: On failure only + +### Test Data + +Test parameters are centralized in `fixtures/test-data.js` and include: + +- Known testnet identity IDs +- Data contract IDs (DPNS, DashPay, etc.) +- Document IDs and examples +- Token IDs for testing +- Parameter sets for each query type + +## Usage Examples + +### Running Specific Tests + +```bash +# Run only identity query tests +npx playwright test --grep "Identity Queries" + +# Run tests for a specific query +npx playwright test --grep "getIdentity" + +# Run tests on headed browser for debugging +npx playwright test --headed --grep "smoke" +``` + +### Adding New Tests + +1. **Add test data** to `fixtures/test-data.js` +2. **Create test file** in `tests/` directory +3. **Use page object** for UI interactions +4. **Use parameter injector** for form filling + +Example: + +```javascript +const { test, expect } = require('@playwright/test'); +const { WasmSdkPage } = require('../utils/wasm-sdk-page'); +const { ParameterInjector } = require('../utils/parameter-injector'); + +test('should execute my new query', async ({ page }) => { + const wasmSdkPage = new WasmSdkPage(page); + const parameterInjector = new ParameterInjector(wasmSdkPage); + + await wasmSdkPage.initialize('testnet'); + await wasmSdkPage.setupQuery('myCategory', 'myQueryType'); + + const success = await parameterInjector.injectParameters('myCategory', 'myQueryType'); + expect(success).toBe(true); + + const result = await wasmSdkPage.executeQueryAndGetResult(); + expect(result.success || result.hasError).toBe(true); +}); +``` + +## CI/CD Integration + +### GitHub Actions Integration + +Tests run automatically in CI or can be triggered manually with different configurations: + +- Automatic execution after WASM SDK builds +- Manual execution with configurable test types and browsers +- Comprehensive reporting with HTML reports and test artifacts + +### Local CI Testing + +For continuous integration, use the test runner script which handles all setup automatically: + +```bash +# In CI environment - the script handles all prerequisites +./run-ui-tests.sh smoke # Quick smoke tests for PR validation +./run-ui-tests.sh all # Full test suite for releases + +# CI-friendly JSON output +DEBUG=false ./run-ui-tests.sh all + +# Results available in: +# - playwright-report/ (HTML) +# - test-results.json (JSON) +# - test-results/ (screenshots, videos) +``` + +**Manual CI setup** (if not using the script): + +```bash +# Install system dependencies +sudo npx playwright install-deps +npx playwright install chromium + +# Run tests with CI-friendly output +npm run test:ci + +# Results will be in test-results/ directory +``` + +## Troubleshooting + +### Common Issues + +1. **Dependencies missing**: Run `sudo npx playwright install-deps` +2. **Port 8888 in use**: Playwright config's `webServer` starts the server automatically; ensure the port is free or update the config +3. **WASM build issues**: The script rebuilds WASM if needed +4. **Test timeouts**: Use `DEBUG=true ./run-ui-tests.sh` for details + +### Debug Mode + +```bash +# See detailed execution logs +DEBUG=true ./run-ui-tests.sh smoke + +# Run with visible browser +./run-ui-tests.sh headed + +# Interactive debugging +./run-ui-tests.sh debug +``` + +## Extending Tests + +To add new tests: + +1. Add test data to `fixtures/test-data.js` +2. Create test cases using the page object model +3. Use `parameter-injector.js` for form filling + +## Known Issues + +- Some queries don't yet support proof information in the WASM SDK +- Tests automatically skip proof testing for unsupported queries +- All core functionality works correctly + +## Support + +For issues or questions: + +1. Use `DEBUG=true ./run-ui-tests.sh` to get detailed execution information +2. Check the HTML reports in `playwright-report/` for visual debugging +3. Review the implementation summary in `IMPLEMENTATION_SUMMARY.md` +4. Examine test screenshots and videos in `test-results/` for failed tests +5. Check GitHub Actions workflow runs for CI/CD issues diff --git a/packages/wasm-sdk/test/ui-automation/fixtures/test-data.js b/packages/wasm-sdk/test/ui-automation/fixtures/test-data.js new file mode 100644 index 00000000000..3f5d4f2edfd --- /dev/null +++ b/packages/wasm-sdk/test/ui-automation/fixtures/test-data.js @@ -0,0 +1,346 @@ +/** + * Test data extracted from existing WASM SDK test parameters + * Based on update_inputs.py and existing test files + */ + +const testData = { + // Known testnet identity IDs for testing (from WASM SDK docs and tests) + identityIds: { + testnet: [ + "5DbLwAxGBzUzo81VewMUwn4b5P4bpv9FNFybi25XB5Bk", // Used in docs.html and multiple test files + "5RG84o6KsTaZudDqS8ytbaRB8QP4YYQ2uwzb6Hj8cfjX" // Used in docs.html + ], + mainnet: [ + // Add mainnet IDs when available + ] + }, + + // Data contract IDs (from WASM SDK files and update_inputs.py) + dataContracts: { + testnet: { + dpns: "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec", // Used in index.html as DPNS_CONTRACT_ID + dashpay: "ALybvzfcCwMs7sinDwmtumw17NneuW7RgFtFHgjKmF3A", + sample: "HLY575cNazmc5824FxqaEMEBuzFeE4a98GDRNKbyJqCM", + tokenPricing: "H7FRpZJqZK933r9CzZMsCuf1BM34NT5P2wSJyjDkprqy", // Used in test-token-pricing-complete.html + tokenContract: "EETVvWgohFDKtbB3ejEzBcDRMNYkc9TtgXY6y8hzP3Ta", // Used in update_inputs.py + postCreate: "9nzpvjVSStUrhkEs3eNHw2JYpcNoLh1MjmqW45QiyjSa" // Used in test_post_create.html + }, + mainnet: { + // Add mainnet contract IDs when available + } + }, + + // Public key hashes for testing + publicKeyHashes: { + testnet: [ + "b7e904ce25ed97594e72f7af0e66f298031c1754", + "518038dc858461bcee90478fd994bba8057b7531" + ] + }, + + // Token IDs for testing + tokenIds: { + testnet: [ + "Hqyu8WcRwXCTwbNxdga4CN5gsVEGc67wng4TFzceyLUv", + "HEv1AYWQfwCffXQgmuzmzyzUo9untRTmVr67n4e4PSWa", // Used in docs.html (last claim) + "4tyvbA2ZGFLvjXLnJRCacSoMbFfpmBwGRrAZsVwnfYri", // Identity 5DbLwAxGBzUzo81VewMUwn4b5P4bpv9FNFybi25XB5Bk frozen + ] + }, + + // ProTx hashes for epoch testing + proTxHashes: { + testnet: [ + "143dcd6a6b7684fde01e88a10e5d65de9a29244c5ecd586d14a342657025f113" + ] + }, + + // Document IDs + documentIds: { + testnet: { + dpnsDomain: "7NYmEKQsYtniQRUmxwdPGeVcirMoPh5ZPyAKz8BWFy3r" + } + }, + + // Specialized balance IDs + specializedBalanceIds: { + testnet: [ + "AzaU7zqCT7X1kxh8yWxkT9PxAgNqWDu4Gz13emwcRyAT" + ] + }, + + // Query test parameters organized by category + queryParameters: { + identity: { + getIdentity: { + testnet: [ + { id: "5DbLwAxGBzUzo81VewMUwn4b5P4bpv9FNFybi25XB5Bk" }, + { id: "5RG84o6KsTaZudDqS8ytbaRB8QP4YYQ2uwzb6Hj8cfjX" } + ] + }, + getIdentityKeys: { + testnet: [ + { + identityId: "5DbLwAxGBzUzo81VewMUwn4b5P4bpv9FNFybi25XB5Bk", + keyRequestType: "all" + }, + { + identityId: "5RG84o6KsTaZudDqS8ytbaRB8QP4YYQ2uwzb6Hj8cfjX", + keyRequestType: "specific", + specificKeyIds: ["1", "2"] + } + ] + }, + getIdentityBalance: { + testnet: [ + { id: "5DbLwAxGBzUzo81VewMUwn4b5P4bpv9FNFybi25XB5Bk" } + ] + }, + getIdentityByPublicKeyHash: { + testnet: [ + { publicKeyHash: "b7e904ce25ed97594e72f7af0e66f298031c1754" } + ] + }, + getIdentitiesContractKeys: { + testnet: [ + { + identitiesIds: [ + "5DbLwAxGBzUzo81VewMUwn4b5P4bpv9FNFybi25XB5Bk", + "5RG84o6KsTaZudDqS8ytbaRB8QP4YYQ2uwzb6Hj8cfjX" + ], + contractId: "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec", + documentTypeName: "domain", + keyRequestType: "all" + } + ] + }, + getIdentityNonce: { + testnet: [ + { identityId: "5DbLwAxGBzUzo81VewMUwn4b5P4bpv9FNFybi25XB5Bk" } + ] + }, + getIdentityContractNonce: { + testnet: [ + { + identityId: "5DbLwAxGBzUzo81VewMUwn4b5P4bpv9FNFybi25XB5Bk", + contractId: "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec" + } + ] + }, + getIdentitiesBalances: { + testnet: [ + { + identityIds: [ + "5DbLwAxGBzUzo81VewMUwn4b5P4bpv9FNFybi25XB5Bk", + "5RG84o6KsTaZudDqS8ytbaRB8QP4YYQ2uwzb6Hj8cfjX" + ] + } + ] + }, + getIdentityBalanceAndRevision: { + testnet: [ + { id: "5DbLwAxGBzUzo81VewMUwn4b5P4bpv9FNFybi25XB5Bk" } + ] + }, + getIdentityByNonUniquePublicKeyHash: { + testnet: [ + { publicKeyHash: "518038dc858461bcee90478fd994bba8057b7531" } + ] + }, + getIdentityTokenBalances: { + testnet: [ + { + identityId: "5DbLwAxGBzUzo81VewMUwn4b5P4bpv9FNFybi25XB5Bk", + tokenIds: [ + "Hqyu8WcRwXCTwbNxdga4CN5gsVEGc67wng4TFzceyLUv", + "HEv1AYWQfwCffXQgmuzmzyzUo9untRTmVr67n4e4PSWa" + ] + } + ] + }, + getIdentitiesTokenBalances: { + testnet: [ + { + identityIds: [ + "5DbLwAxGBzUzo81VewMUwn4b5P4bpv9FNFybi25XB5Bk", + "5RG84o6KsTaZudDqS8ytbaRB8QP4YYQ2uwzb6Hj8cfjX" + ], + tokenId: "Hqyu8WcRwXCTwbNxdga4CN5gsVEGc67wng4TFzceyLUv" + } + ] + }, + getIdentityTokenInfos: { + testnet: [ + { + identityId: "5DbLwAxGBzUzo81VewMUwn4b5P4bpv9FNFybi25XB5Bk", + tokenIds: [ + "Hqyu8WcRwXCTwbNxdga4CN5gsVEGc67wng4TFzceyLUv", + "4tyvbA2ZGFLvjXLnJRCacSoMbFfpmBwGRrAZsVwnfYri" + ] + } + ] + }, + getIdentitiesTokenInfos: { + testnet: [ + { + identityIds: [ + "5DbLwAxGBzUzo81VewMUwn4b5P4bpv9FNFybi25XB5Bk", + "5RG84o6KsTaZudDqS8ytbaRB8QP4YYQ2uwzb6Hj8cfjX" + ], + tokenId: "4tyvbA2ZGFLvjXLnJRCacSoMbFfpmBwGRrAZsVwnfYri" + } + ] + } + }, + + dataContract: { + getDataContract: { + testnet: [ + { id: "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec" }, + { id: "ALybvzfcCwMs7sinDwmtumw17NneuW7RgFtFHgjKmF3A" } + ] + }, + getDataContracts: { + testnet: [ + { + ids: [ + "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec", + "ALybvzfcCwMs7sinDwmtumw17NneuW7RgFtFHgjKmF3A" + ] + } + ] + }, + getDataContractHistory: { + testnet: [ + { + id: "HLY575cNazmc5824FxqaEMEBuzFeE4a98GDRNKbyJqCM", + limit: 10, + offset: 0 + } + ] + } + }, + + document: { + getDocuments: { + testnet: [ + { + dataContractId: "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec", + documentType: "domain", + limit: 10 + } + ] + }, + getDocument: { + testnet: [ + { + dataContractId: "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec", + documentType: "domain", + documentId: "7NYmEKQsYtniQRUmxwdPGeVcirMoPh5ZPyAKz8BWFy3r" + } + ] + } + }, + + system: { + getStatus: { + testnet: [{}] // No parameters needed + }, + getTotalCreditsInPlatform: { + testnet: [{}] + } + }, + + protocol: { + getProtocolVersionUpgradeState: { + testnet: [{}] + } + }, + + epoch: { + getCurrentEpoch: { + testnet: [{}] + }, + getEpochsInfo: { + testnet: [ + { + epoch: 1000, + count: 5, + ascending: true + } + ] + }, + getEvonodesProposedEpochBlocksByIds: { + testnet: [ + { + ids: ["143dcd6a6b7684fde01e88a10e5d65de9a29244c5ecd586d14a342657025f113"] + } + ] + } + }, + + token: { + getTokenStatuses: { + testnet: [ + { + tokenIds: ["Hqyu8WcRwXCTwbNxdga4CN5gsVEGc67wng4TFzceyLUv"] + } + ] + } + } + }, + + // Common where clauses for document queries + whereClausesExamples: { + dpnsDomain: [ + [["normalizedParentDomainName", "==", "dash"]], + [["normalizedParentDomainName", "==", "dash"], ["normalizedLabel", "startsWith", "test"]] + ] + }, + + // Order by examples + orderByExamples: { + createdAtDesc: [["$createdAt", "desc"]], + createdAtAsc: [["$createdAt", "asc"]] + } +}; + +/** + * Get test parameters for a specific query + */ +function getTestParameters(category, queryType, network = 'testnet') { + const categoryData = testData.queryParameters[category]; + if (!categoryData) { + throw new Error(`No test data found for category: ${category}`); + } + + const queryData = categoryData[queryType]; + if (!queryData) { + throw new Error(`No test data found for query: ${category}.${queryType}`); + } + + const networkData = queryData[network]; + if (!networkData || networkData.length === 0) { + throw new Error(`No test data found for ${category}.${queryType} on ${network}`); + } + + return networkData[0]; // Return first test case +} + +/** + * Get all test parameters for a query (for parameterized testing) + */ +function getAllTestParameters(category, queryType, network = 'testnet') { + const categoryData = testData.queryParameters[category]; + if (!categoryData) return []; + + const queryData = categoryData[queryType]; + if (!queryData) return []; + + return queryData[network] || []; +} + +module.exports = { + testData, + getTestParameters, + getAllTestParameters +}; diff --git a/packages/wasm-sdk/test/ui-automation/package-lock.json b/packages/wasm-sdk/test/ui-automation/package-lock.json new file mode 100644 index 00000000000..7c1d8959081 --- /dev/null +++ b/packages/wasm-sdk/test/ui-automation/package-lock.json @@ -0,0 +1,81 @@ +{ + "name": "wasm-sdk-ui-automation", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "wasm-sdk-ui-automation", + "version": "1.0.0", + "devDependencies": { + "@playwright/test": "^1.41.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@playwright/test": { + "version": "1.54.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.1.tgz", + "integrity": "sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.54.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "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.54.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.1.tgz", + "integrity": "sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.54.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.54.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.1.tgz", + "integrity": "sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/packages/wasm-sdk/test/ui-automation/package.json b/packages/wasm-sdk/test/ui-automation/package.json new file mode 100644 index 00000000000..8785b6257c4 --- /dev/null +++ b/packages/wasm-sdk/test/ui-automation/package.json @@ -0,0 +1,28 @@ +{ + "name": "wasm-sdk-ui-automation", + "version": "1.0.0", + "description": "UI automation tests for WASM SDK web interface", + "private": true, + "scripts": { + "test": "playwright test", + "test:headed": "playwright test --headed", + "test:debug": "playwright test --debug", + "test:ui": "playwright test --ui", + "test:report": "playwright show-report", + "test:smoke": "playwright test tests/basic-smoke.spec.js", + "test:queries": "playwright test tests/query-execution.spec.js", + "test:parameterized": "playwright test tests/parameterized-queries.spec.js", + "test:all": "playwright test --reporter=html,json,list", + "test:ci": "playwright test --reporter=json --output-dir=test-results", + "install-browsers": "playwright install", + "serve": "cd ../../ && python3 -m http.server 8888", + "pretest": "echo 'Starting web server for tests...'", + "posttest": "echo 'Tests completed. Check playwright-report/ for results.'" + }, + "devDependencies": { + "@playwright/test": "^1.54.1" + }, + "engines": { + "node": ">=18" + } +} diff --git a/packages/wasm-sdk/test/ui-automation/playwright.config.js b/packages/wasm-sdk/test/ui-automation/playwright.config.js new file mode 100644 index 00000000000..4e8c47a7400 --- /dev/null +++ b/packages/wasm-sdk/test/ui-automation/playwright.config.js @@ -0,0 +1,74 @@ +// @ts-check +const { defineConfig, devices } = require('@playwright/test'); + +/** + * @see https://playwright.dev/docs/test-configuration + */ +module.exports = defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: [ + ['html', { outputFolder: 'playwright-report' }], + ['json', { outputFile: 'test-results.json' }], + ['list'] + ], + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: 'http://localhost:8888', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + + /* Take screenshot on failure */ + screenshot: 'only-on-failure', + + /* Record video on failure */ + video: 'retain-on-failure', + + /* Global timeout for each action (e.g. click, fill, etc.) */ + actionTimeout: 30000, + + /* Global timeout for each navigation action */ + navigationTimeout: 30000, + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + // Enable headless mode by default + headless: true, + // Use a larger viewport for better testing + viewport: { width: 1920, height: 1080 } + }, + }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: 'python3 -m http.server 8888', + url: 'http://localhost:8888', + cwd: '../../', // Run from wasm-sdk directory to serve index.html + reuseExistingServer: !process.env.CI, + timeout: 60000, + }, + + /* Global test timeout */ + timeout: 120000, + + /* Expect timeout for assertions */ + expect: { + timeout: 10000, + }, +}); diff --git a/packages/wasm-sdk/test/ui-automation/run-ui-tests.sh b/packages/wasm-sdk/test/ui-automation/run-ui-tests.sh new file mode 100755 index 00000000000..808a3f1050c --- /dev/null +++ b/packages/wasm-sdk/test/ui-automation/run-ui-tests.sh @@ -0,0 +1,288 @@ +#!/bin/bash + +# WASM SDK UI Automation Test Runner +# This script sets up and runs the UI automation tests + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Configuration +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WASM_SDK_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" +UI_TEST_DIR="$SCRIPT_DIR" + +# Debug mode flag +DEBUG=${DEBUG:-false} + +# Function to print colored output +print_status() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +print_debug() { + if [ "$DEBUG" = "true" ]; then + echo -e "${YELLOW}[DEBUG]${NC} $1" + fi +} + + +# Function to validate paths +validate_paths() { + print_debug "Validating directory paths..." + print_debug "SCRIPT_DIR: $SCRIPT_DIR" + print_debug "WASM_SDK_DIR: $WASM_SDK_DIR" + print_debug "UI_TEST_DIR: $UI_TEST_DIR" + + # Check if UI test directory exists and contains expected files + if [ ! -d "$UI_TEST_DIR" ]; then + print_error "UI test directory not found: $UI_TEST_DIR" + exit 1 + fi + + if [ ! -f "$UI_TEST_DIR/package.json" ]; then + print_error "package.json not found in UI test directory: $UI_TEST_DIR" + exit 1 + fi + + if [ ! -f "$UI_TEST_DIR/playwright.config.js" ]; then + print_error "playwright.config.js not found in UI test directory: $UI_TEST_DIR" + exit 1 + fi + + # Check if WASM SDK directory exists + if [ ! -d "$WASM_SDK_DIR" ]; then + print_error "WASM SDK directory not found: $WASM_SDK_DIR" + exit 1 + fi + + if [ ! -f "$WASM_SDK_DIR/index.html" ]; then + print_error "index.html not found in WASM SDK directory: $WASM_SDK_DIR" + exit 1 + fi + + print_debug "Path validation passed โœ“" +} + +# Function to check prerequisites +check_prerequisites() { + print_status "Checking prerequisites..." + + # Validate paths first + validate_paths + + # Check Node.js + if ! command -v node &> /dev/null; then + print_error "Node.js is not installed. Please install Node.js 18+ and try again." + exit 1 + fi + + NODE_VERSION=$(node --version | sed 's/v//') + NODE_MAJOR=$(echo $NODE_VERSION | cut -d. -f1) + if [ "$NODE_MAJOR" -lt 18 ]; then + print_error "Node.js version $NODE_VERSION is too old. Please install Node.js 18+ and try again." + exit 1 + fi + print_debug "Node.js version: $NODE_VERSION โœ“" + + # Check Python + if ! command -v python3 &> /dev/null; then + print_error "Python 3 is not installed. Please install Python 3 and try again." + exit 1 + fi + PYTHON_VERSION=$(python3 --version 2>&1 | cut -d' ' -f2) + print_debug "Python version: $PYTHON_VERSION โœ“" + + # Check if WASM SDK is built + if [ ! -f "$WASM_SDK_DIR/pkg/wasm_sdk.js" ]; then + print_warning "WASM SDK not found. Building..." + cd "$WASM_SDK_DIR" + if ! ./build.sh; then + print_error "Failed to build WASM SDK" + exit 1 + fi + fi + print_debug "WASM SDK found โœ“" + + print_status "Prerequisites check passed โœ“" +} + +# Function to install dependencies +install_dependencies() { + print_status "Installing test dependencies..." + + cd "$UI_TEST_DIR" || { + print_error "Failed to change to UI test directory: $UI_TEST_DIR" + exit 1 + } + print_debug "Changed to directory: $(pwd)" + + if [ ! -d "node_modules" ]; then + print_status "Installing npm dependencies..." + if ! npm install; then + print_error "Failed to install npm dependencies" + exit 1 + fi + fi + + # Check if browsers are installed + if ! npx playwright --version &> /dev/null; then + print_error "Playwright not found. Installing..." + if ! npm install; then + print_error "Failed to install Playwright" + exit 1 + fi + fi + + # Install browsers if needed + if ! find "$HOME/.cache/ms-playwright" -maxdepth 1 -name "chromium-*" -type d -print -quit 2>/dev/null | grep -q .; then + print_status "Installing Playwright browsers..." + if ! npx playwright install chromium; then + print_error "Failed to install Playwright browsers" + exit 1 + fi + fi + + print_status "Dependencies installed โœ“" +} + + +# Function to run tests +run_tests() { + print_status "Running UI automation tests..." + + cd "$UI_TEST_DIR" || { + print_error "Failed to change to UI test directory: $UI_TEST_DIR" + exit 1 + } + print_debug "Running tests from directory: $(pwd)" + + # Verify npm scripts exist + if [ ! -f "package.json" ]; then + print_error "package.json not found in test directory" + exit 1 + fi + + # Show available npm scripts for debugging + print_debug "Available npm scripts:" + if [ "$DEBUG" = "true" ]; then + npm run 2>/dev/null | grep -E "^\s*(test:|build:|start)" || true + fi + + # Determine test type from arguments + case "${1:-all}" in + "smoke") + print_status "Running smoke tests..." + npm run test:smoke + ;; + "queries") + print_status "Running query execution tests..." + npm run test:queries + ;; + "parameterized") + print_status "Running parameterized tests..." + npm run test:parameterized + ;; + "headed") + print_status "Running tests in headed mode..." + npm run test:headed + ;; + "debug") + print_status "Running tests in debug mode..." + npm run test:debug + ;; + "ui") + print_status "Running tests in UI mode..." + npm run test:ui + ;; + "all") + print_status "Running all tests..." + npm run test:all + ;; + *) + # Pass through any other arguments to playwright + print_status "Running custom playwright command: $*" + npx playwright test "$@" + ;; + esac +} + +# Function to show results +show_results() { + print_status "Test execution completed!" + + if [ -d "$UI_TEST_DIR/playwright-report" ]; then + print_status "HTML report available at: $UI_TEST_DIR/playwright-report/index.html" + print_status "To view report: npm run test:report" + fi + + if [ -f "$UI_TEST_DIR/test-results.json" ]; then + print_status "JSON results available at: $UI_TEST_DIR/test-results.json" + fi +} + +# Function to print usage +print_usage() { + echo "Usage: $0 [test_type]" + echo "" + echo "Test types:" + echo " smoke - Run basic smoke tests" + echo " queries - Run query execution tests" + echo " parameterized - Run parameterized tests" + echo " headed - Run tests in headed mode (visible browser)" + echo " debug - Run tests in debug mode" + echo " ui - Run tests in UI mode (interactive)" + echo " all - Run all tests (default)" + echo "" + echo "Environment variables:" + echo " DEBUG=true - Enable debug output" + echo "" + echo "Examples:" + echo " $0 # Run all tests" + echo " $0 smoke # Run smoke tests only" + echo " $0 headed # Run tests with visible browser" + echo " DEBUG=true $0 smoke # Run smoke tests with debug output" + echo " $0 --grep=\"Identity\" # Run tests matching pattern" +} + +# Main execution +main() { + # Handle help flag + if [[ "$1" == "-h" || "$1" == "--help" ]]; then + print_usage + exit 0 + fi + + print_status "Starting WASM SDK UI Automation Tests..." + print_status "Working directory: $WASM_SDK_DIR" + print_status "Test directory: $UI_TEST_DIR" + + check_prerequisites + install_dependencies + + # Run tests and capture exit code + if run_tests "$@"; then + print_status "All tests completed successfully! โœ…" + show_results + exit 0 + else + print_error "Some tests failed! โŒ" + show_results + exit 1 + fi +} + +# Run main function with all arguments +main "$@" diff --git a/packages/wasm-sdk/test/ui-automation/tests/basic-smoke.spec.js b/packages/wasm-sdk/test/ui-automation/tests/basic-smoke.spec.js new file mode 100644 index 00000000000..576900d5751 --- /dev/null +++ b/packages/wasm-sdk/test/ui-automation/tests/basic-smoke.spec.js @@ -0,0 +1,182 @@ +const { test, expect } = require('@playwright/test'); +const { WasmSdkPage } = require('../utils/wasm-sdk-page'); + +test.describe('WASM SDK Basic Smoke Tests', () => { + let wasmSdkPage; + + test.beforeEach(async ({ page }) => { + wasmSdkPage = new WasmSdkPage(page); + await wasmSdkPage.initialize('testnet'); + }); + + test('should initialize SDK successfully', async () => { + // Wait for SDK to be fully ready (with retry logic) + let statusState; + let attempts = 0; + const maxAttempts = 3; + + while (attempts < maxAttempts) { + statusState = await wasmSdkPage.getStatusBannerState(); + + if (statusState === 'success') { + break; + } + + if (statusState === 'loading') { + // Wait for loading to complete + await wasmSdkPage.waitForSdkReady(); + statusState = await wasmSdkPage.getStatusBannerState(); + + if (statusState === 'success') { + break; + } + } + + attempts++; + if (attempts < maxAttempts) { + await wasmSdkPage.page.waitForTimeout(2000); + } + } + + // Final check + expect(statusState).toBe('success'); + + // Verify network is set to testnet + const networkIndicator = wasmSdkPage.page.locator('#networkIndicator'); + await expect(networkIndicator).toContainText('TESTNET'); + }); + + test('should load query categories', async () => { + await wasmSdkPage.setOperationType('queries'); + + const categories = await wasmSdkPage.getAvailableQueryCategories(); + + // Check that we have the expected categories + const expectedCategories = [ + 'Identity Queries', + 'Data Contract Queries', + 'Document Queries', + 'DPNS Queries', + 'Voting & Contested Resources', + 'Protocol & Version', + 'Epoch & Block', + 'Token Queries', + 'Group Queries', + 'System & Utility' + ]; + + for (const category of expectedCategories) { + expect(categories).toContain(category); + } + }); + + test('should switch between networks', async () => { + // Test switching to mainnet + await wasmSdkPage.setNetwork('mainnet'); + const mainnetIndicator = wasmSdkPage.page.locator('#networkIndicator'); + await expect(mainnetIndicator).toContainText('MAINNET'); + + // Switch back to testnet + await wasmSdkPage.setNetwork('testnet'); + const testnetIndicator = wasmSdkPage.page.locator('#networkIndicator'); + await expect(testnetIndicator).toContainText('TESTNET'); + }); + + test('should show query types when category is selected', async () => { + await wasmSdkPage.setOperationType('queries'); + await wasmSdkPage.setQueryCategory('identity'); + + const queryTypes = await wasmSdkPage.getAvailableQueryTypes(); + + // Should have some identity query types + expect(queryTypes.length).toBeGreaterThan(0); + expect(queryTypes).toContain('Get Identity'); + }); + + test('should show input fields when query type is selected', async () => { + await wasmSdkPage.setOperationType('queries'); + await wasmSdkPage.setQueryCategory('identity'); + await wasmSdkPage.setQueryType('getIdentity'); + + // Should show query inputs container + const queryInputs = wasmSdkPage.page.locator('#queryInputs'); + await expect(queryInputs).toBeVisible(); + + // Should show execute button + const executeButton = wasmSdkPage.page.locator('#executeQuery'); + await expect(executeButton).toBeVisible(); + }); + + test('should enable/disable execute button based on form completion', async () => { + await wasmSdkPage.setOperationType('queries'); + await wasmSdkPage.setQueryCategory('identity'); + await wasmSdkPage.setQueryType('getIdentity'); + + const executeButton = wasmSdkPage.page.locator('#executeQuery'); + + // Button should be enabled (even without required params for this test) + await expect(executeButton).toBeVisible(); + }); + + test('should clear results when clear button is clicked', async () => { + await wasmSdkPage.setOperationType('queries'); + await wasmSdkPage.setQueryCategory('system'); + await wasmSdkPage.setQueryType('getStatus'); + + // Execute a simple query first + await wasmSdkPage.executeQuery(); + + // Clear results + await wasmSdkPage.clearResults(); + + // Verify results are cleared + const resultContent = wasmSdkPage.page.locator('#identityInfo'); + await expect(resultContent).toHaveClass(/empty/); + }); + + test('should toggle proof information', async () => { + await wasmSdkPage.setOperationType('queries'); + await wasmSdkPage.setQueryCategory('identity'); + await wasmSdkPage.setQueryType('getIdentity'); + + // Wait a moment for UI to fully load + await wasmSdkPage.page.waitForTimeout(1000); + + // Check if proof toggle is available + const proofContainer = wasmSdkPage.page.locator('#proofToggleContainer'); + + try { + // Wait for container to potentially appear + await proofContainer.waitFor({ state: 'visible', timeout: 5000 }); + + // Test enabling proof info + const enableSuccess = await wasmSdkPage.enableProofInfo(); + if (enableSuccess) { + const proofToggle = wasmSdkPage.page.locator('#proofToggle'); + await expect(proofToggle).toBeChecked(); + + // Test disabling proof info + const disableSuccess = await wasmSdkPage.disableProofInfo(); + if (disableSuccess) { + await expect(proofToggle).not.toBeChecked(); + } + + } else { + } + } catch (error) { + // Proof toggle not available for this query type - that's OK + } + }); + + test('should show query description when available', async () => { + await wasmSdkPage.setOperationType('queries'); + await wasmSdkPage.setQueryCategory('identity'); + await wasmSdkPage.setQueryType('getIdentity'); + + const description = await wasmSdkPage.getQueryDescription(); + + if (description) { + expect(description.length).toBeGreaterThan(0); + } + }); +}); diff --git a/packages/wasm-sdk/test/ui-automation/tests/parameterized-queries.spec.js b/packages/wasm-sdk/test/ui-automation/tests/parameterized-queries.spec.js new file mode 100644 index 00000000000..f1f2ee07717 --- /dev/null +++ b/packages/wasm-sdk/test/ui-automation/tests/parameterized-queries.spec.js @@ -0,0 +1,238 @@ +const { test, expect } = require('@playwright/test'); +const { WasmSdkPage } = require('../utils/wasm-sdk-page'); +const { ParameterInjector } = require('../utils/parameter-injector'); + +test.describe('WASM SDK Parameterized Query Tests', () => { + let wasmSdkPage; + let parameterInjector; + + test.beforeEach(async ({ page }) => { + wasmSdkPage = new WasmSdkPage(page); + parameterInjector = new ParameterInjector(wasmSdkPage); + await wasmSdkPage.initialize('testnet'); + }); + + // Generate parameterized tests for each query category + const queryCategories = [ + { category: 'identity', queries: ['getIdentity', 'getIdentityBalance', 'getIdentityKeys'] }, + { category: 'dataContract', queries: ['getDataContract', 'getDataContracts'] }, + { category: 'document', queries: ['getDocuments', 'getDocument'] }, + { category: 'system', queries: ['getStatus', 'getCurrentEpoch', 'getTotalCreditsInPlatform'] } + ]; + + for (const { category, queries } of queryCategories) { + test.describe(`${category.toUpperCase()} Category Tests`, () => { + + for (const queryType of queries) { + test(`should execute ${queryType} with all available parameter sets`, async () => { + const parameterSets = parameterInjector.createParameterizedTests(category, queryType, 'testnet'); + + if (parameterSets.length === 0) { + test.skip(`No parameter sets available for ${category}.${queryType}`); + return; + } + + let successCount = 0; + let errorCount = 0; + const results = []; + + for (const paramSet of parameterSets) { + try { + console.log(`\n๐Ÿงช Testing ${paramSet.testName}`); + + await wasmSdkPage.setupQuery(category, queryType); + + // Inject parameters + const injectionSuccess = await parameterInjector.injectParameters( + category, + queryType, + 'testnet', + paramSet.index + ); + + if (!injectionSuccess) { + console.warn(`โš ๏ธ Could not inject parameters for ${paramSet.testName}`); + continue; + } + + // Execute query + const result = await wasmSdkPage.executeQueryAndGetResult(); + results.push({ + testName: paramSet.testName, + parameters: paramSet.parameters, + success: result.success, + hasError: result.hasError, + resultLength: result.result?.length || 0, + statusText: result.statusText + }); + + if (result.success) { + successCount++; + console.log(`โœ… ${paramSet.testName} - SUCCESS`); + } else { + errorCount++; + console.log(`โŒ ${paramSet.testName} - ERROR: ${result.statusText}`); + } + + // Brief pause between executions + await wasmSdkPage.page.waitForTimeout(1000); + await wasmSdkPage.clearResults(); + + } catch (error) { + errorCount++; + console.error(`๐Ÿ’ฅ ${paramSet.testName} - EXCEPTION:`, error.message); + results.push({ + testName: paramSet.testName, + parameters: paramSet.parameters, + success: false, + hasError: true, + error: error.message + }); + } + } + + // Summary assertions + console.log(`\n๐Ÿ“Š ${category}.${queryType} Summary:`); + console.log(` Total tests: ${parameterSets.length}`); + console.log(` Successful: ${successCount}`); + console.log(` Errors: ${errorCount}`); + + // At least one test should complete (success or graceful error) + expect(successCount + errorCount).toBeGreaterThan(0); + + // Store results for reporting + test.info().attach('test-results', { + body: JSON.stringify(results, null, 2), + contentType: 'application/json' + }); + }); + } + }); + } + + test.describe('Cross-Network Parameter Tests', () => { + const networks = ['testnet', 'mainnet']; + + for (const network of networks) { + test(`should execute system queries on ${network}`, async () => { + await wasmSdkPage.setNetwork(network); + + const systemQueries = ['getStatus', 'getCurrentEpoch']; + const results = []; + + for (const queryType of systemQueries) { + try { + await wasmSdkPage.setupQuery('system', queryType); + const result = await wasmSdkPage.executeQueryAndGetResult(); + + results.push({ + network, + queryType, + success: result.success, + hasError: result.hasError, + resultLength: result.result?.length || 0 + }); + + await wasmSdkPage.clearResults(); + await wasmSdkPage.page.waitForTimeout(500); + + } catch (error) { + results.push({ + network, + queryType, + success: false, + error: error.message + }); + } + } + + // At least one query should work + const successfulQueries = results.filter(r => r.success); + expect(successfulQueries.length).toBeGreaterThan(0); + + console.log(`${network} system queries:`, results); + }); + } + }); + + test.describe('Parameter Validation Tests', () => { + test('should validate parameters before injection', async () => { + const testCases = [ + { + category: 'identity', + queryType: 'getIdentity', + parameters: { id: 'GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec' }, + expectedValid: true + }, + { + category: 'identity', + queryType: 'getIdentity', + parameters: { id: '' }, + expectedValid: false + }, + { + category: 'identity', + queryType: 'getIdentity', + parameters: { id: '1234567890' }, // Too short for base58 ID + expectedValid: false + }, + { + category: 'identity', + queryType: 'getIdentity', + parameters: { id: 'GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S0Il' }, // Contains invalid base58 chars + expectedValid: false + } + ]; + + for (const testCase of testCases) { + const validation = parameterInjector.validateParameters(testCase.parameters); + + if (testCase.expectedValid) { + expect(validation.errors.length).toBe(0); + } else { + expect(validation.errors.length).toBeGreaterThan(0); + } + + console.log(`Validation for ${JSON.stringify(testCase.parameters)}:`, validation); + } + }); + }); + + test.describe('Random Parameter Stress Tests', () => { + test('should handle random parameter generation gracefully', async () => { + const testQueries = [ + { category: 'identity', queryType: 'getIdentity' }, + { category: 'system', queryType: 'getStatus' } + ]; + + for (const { category, queryType } of testQueries) { + try { + const randomParams = parameterInjector.generateRandomParameters(category, queryType); + + if (Object.keys(randomParams).length > 0) { + await wasmSdkPage.setupQuery(category, queryType); + await wasmSdkPage.fillQueryParameters(randomParams); + + const result = await wasmSdkPage.executeQueryAndGetResult(false); + + // Should complete without crashing + expect(result).toBeDefined(); + expect(typeof result.success).toBe('boolean'); + + console.log(`Random test ${category}.${queryType}:`, { + parameters: randomParams, + success: result.success, + hasError: result.hasError + }); + } + + await wasmSdkPage.clearResults(); + + } catch (error) { + // Random parameters might cause errors - that's OK as long as UI doesn't crash + console.log(`Random test error (acceptable): ${error.message}`); + } + } + }); + }); +}); diff --git a/packages/wasm-sdk/test/ui-automation/tests/query-execution.spec.js b/packages/wasm-sdk/test/ui-automation/tests/query-execution.spec.js new file mode 100644 index 00000000000..0eb2041bcd9 --- /dev/null +++ b/packages/wasm-sdk/test/ui-automation/tests/query-execution.spec.js @@ -0,0 +1,639 @@ +const { test, expect } = require('@playwright/test'); +const { WasmSdkPage } = require('../utils/wasm-sdk-page'); +const { ParameterInjector } = require('../utils/parameter-injector'); + +/** + * Helper function to execute a query with proof toggle enabled + * @param {WasmSdkPage} wasmSdkPage - The page object instance + * @param {ParameterInjector} parameterInjector - The parameter injector instance + * @param {string} category - Query category (e.g., 'identity', 'documents') + * @param {string} queryName - Query name (e.g., 'getIdentity') + * @param {string} network - Network to use ('testnet' or 'mainnet') + * @returns {Promise} - The query result object + */ +async function executeQueryWithProof(wasmSdkPage, parameterInjector, category, queryName, network = 'testnet') { + await wasmSdkPage.setupQuery(category, queryName); + + // Enable proof info if available + const proofEnabled = await wasmSdkPage.enableProofInfo(); + + // If proof was enabled, wait for the toggle to be actually checked + if (proofEnabled) { + const proofToggle = wasmSdkPage.page.locator('#proofToggle'); + await expect(proofToggle).toBeChecked(); + console.log('Proof toggle confirmed as checked'); + } + + const success = await parameterInjector.injectParameters(category, queryName, network); + expect(success).toBe(true); + + const result = await wasmSdkPage.executeQueryAndGetResult(); + + return { result, proofEnabled }; +} + +/** + * Helper function to parse balance/nonce responses that may contain large numbers + * @param {string} resultStr - The raw result string from the query + * @param {string} propertyName - The property name to extract (e.g., 'balance', 'nonce') + * @returns {number} - The parsed number value + */ +function parseNumericResult(resultStr, propertyName = 'balance') { + const trimmedStr = resultStr.trim(); + + // Try to parse as JSON first (in case it's a JSON response) + let numericValue; + try { + const parsed = JSON.parse(trimmedStr); + + // Check if it's a JSON object with the expected property + if (typeof parsed === 'object' && parsed[propertyName] !== undefined) { + numericValue = Number(parsed[propertyName]); + } else if (typeof parsed === 'number') { + numericValue = parsed; + } else { + numericValue = Number(parsed); + } + } catch { + // If not JSON, try parsing directly as number + numericValue = Number(trimmedStr); + + // If Number() fails, log the issue + if (isNaN(numericValue)) { + console.error(`Failed to parse ${propertyName}:`, trimmedStr, 'type:', typeof trimmedStr); + } + } + + return numericValue; +} + +/** + * Helper function to validate basic query result properties + * @param {Object} result - The query result object + */ +function validateBasicQueryResult(result) { + expect(result.success).toBe(true); + expect(result.result).toBeDefined(); + expect(result.hasError).toBe(false); + expect(result.result).not.toContain('Error executing query'); + expect(result.result).not.toContain('not found'); + expect(result.result).not.toContain('invalid'); +} + +/** + * Helper function to validate proof content contains expected fields + * @param {string} proofContent - The proof content string + */ +function validateProofContent(proofContent) { + expect(proofContent).toBeDefined(); + expect(proofContent).not.toBe(''); + expect(proofContent).toContain('metadata'); + expect(proofContent).toContain('proof'); + expect(proofContent).toContain('grovedbProof'); + expect(proofContent).toContain('quorumHash'); + expect(proofContent).toContain('signature'); +} + +/** + * Helper function to validate split view (proof mode) result + * @param {Object} result - The query result object + */ +function validateSplitView(result) { + expect(result.inSplitView).toBe(true); + expect(result.proofContent).toBeDefined(); + expect(result.proofContent).not.toBe(''); + validateProofContent(result.proofContent); +} + +/** + * Helper function to validate single view (non-proof mode) result + * @param {Object} result - The query result object + */ +function validateSingleView(result) { + expect(result.inSplitView).toBe(false); + expect(result.proofContent).toBeNull(); +} + +/** + * Helper function to validate data contract result + * @param {string} resultStr - The raw result string containing contract data + */ +function validateContractResult(resultStr) { + expect(() => JSON.parse(resultStr)).not.toThrow(); + const contractData = JSON.parse(resultStr); + expect(contractData).toBeDefined(); + expect(contractData).toHaveProperty('id'); + expect(contractData).toHaveProperty('config'); +} + +/** + * Helper function to validate document result + * @param {string} resultStr - The raw result string containing document data + */ +function validateDocumentResult(resultStr) { + expect(() => JSON.parse(resultStr)).not.toThrow(); + const documentData = JSON.parse(resultStr); + expect(documentData).toBeDefined(); + // Documents can be arrays or single objects + if (Array.isArray(documentData)) { + expect(documentData.length).toBeGreaterThanOrEqual(0); + } else { + expect(documentData).toBeInstanceOf(Object); + } +} + +/** + * Helper function to validate numeric results and ensure they're valid + * @param {string} resultStr - The raw result string + * @param {string} propertyName - The property name to extract + * @returns {number} - The validated numeric value + */ +function validateNumericResult(resultStr, propertyName = 'balance') { + const numericValue = parseNumericResult(resultStr, propertyName); + expect(numericValue).not.toBeNaN(); + expect(numericValue).toBeGreaterThanOrEqual(0); + return numericValue; +} + +/** + * Specific validation functions for parameterized tests + */ +function validateIdentityResult(resultStr) { + expect(() => JSON.parse(resultStr)).not.toThrow(); + const identityData = JSON.parse(resultStr); + expect(identityData).toHaveProperty('id'); + expect(identityData).toHaveProperty('publicKeys'); + expect(identityData).toHaveProperty('balance'); +} + +function validateKeysResult(resultStr) { + expect(() => JSON.parse(resultStr)).not.toThrow(); + const keysData = JSON.parse(resultStr); + expect(keysData).toBeDefined(); +} + +function validateIdentitiesResult(resultStr) { + expect(() => JSON.parse(resultStr)).not.toThrow(); + const identitiesData = JSON.parse(resultStr); + expect(identitiesData).toBeDefined(); + + if (Array.isArray(identitiesData)) { + expect(identitiesData.length).toBeGreaterThanOrEqual(0); + // Validate each identity using the single identity validator + identitiesData.forEach(identity => { + validateIdentityResult(JSON.stringify(identity)); + }); + } else { + // Single identity - use the existing validator + validateIdentityResult(JSON.stringify(identitiesData)); + } +} + +function validateBalancesResult(resultStr) { + expect(() => JSON.parse(resultStr)).not.toThrow(); + const balancesData = JSON.parse(resultStr); + expect(balancesData).toBeDefined(); + if (Array.isArray(balancesData)) { + expect(balancesData.length).toBeGreaterThanOrEqual(0); + } +} + +function validateBalanceAndRevisionResult(resultStr) { + expect(() => JSON.parse(resultStr)).not.toThrow(); + const data = JSON.parse(resultStr); + expect(data).toBeDefined(); + expect(data).toBeInstanceOf(Object); +} + +function validateTokenBalanceResult(resultStr) { + expect(() => JSON.parse(resultStr)).not.toThrow(); + const tokenData = JSON.parse(resultStr); + expect(tokenData).toBeDefined(); +} + +function validateTokenInfoResult(resultStr) { + expect(() => JSON.parse(resultStr)).not.toThrow(); + const tokenInfoData = JSON.parse(resultStr); + expect(tokenInfoData).toBeDefined(); +} + +test.describe('WASM SDK Query Execution Tests', () => { + let wasmSdkPage; + let parameterInjector; + + test.beforeEach(async ({ page }) => { + wasmSdkPage = new WasmSdkPage(page); + parameterInjector = new ParameterInjector(wasmSdkPage); + await wasmSdkPage.initialize('testnet'); + }); + + test.describe('Data Contract Queries', () => { + test('should execute getDataContract query', async () => { + await wasmSdkPage.setupQuery('dataContract', 'getDataContract'); + + const success = await parameterInjector.injectParameters('dataContract', 'getDataContract', 'testnet'); + expect(success).toBe(true); + + const result = await wasmSdkPage.executeQueryAndGetResult(); + + // Use helper functions for validation + validateBasicQueryResult(result); + validateSingleView(result); + validateContractResult(result.result); + + console.log('โœ… getDataContract single view without proof confirmed'); + }); + + test('should execute getDataContracts query for multiple contracts', async () => { + await wasmSdkPage.setupQuery('dataContract', 'getDataContracts'); + + const success = await parameterInjector.injectParameters('dataContract', 'getDataContracts', 'testnet'); + expect(success).toBe(true); + + const result = await wasmSdkPage.executeQueryAndGetResult(); + + // Use helper functions for validation + validateBasicQueryResult(result); + validateSingleView(result); + + // Multiple contracts result should be valid JSON + expect(() => JSON.parse(result.result)).not.toThrow(); + const contractsData = JSON.parse(result.result); + expect(contractsData).toBeDefined(); + + console.log('โœ… getDataContracts single view without proof confirmed'); + }); + + test('should execute getDataContractHistory query', async () => { + await wasmSdkPage.setupQuery('dataContract', 'getDataContractHistory'); + + const success = await parameterInjector.injectParameters('dataContract', 'getDataContractHistory', 'testnet'); + expect(success).toBe(true); + + const result = await wasmSdkPage.executeQueryAndGetResult(); + + // Use helper functions for validation + validateBasicQueryResult(result); + validateSingleView(result); + + // Contract history should be valid JSON (array of contract versions) + expect(() => JSON.parse(result.result)).not.toThrow(); + const historyData = JSON.parse(result.result); + expect(historyData).toBeDefined(); + expect(Array.isArray(historyData) || typeof historyData === 'object').toBe(true); + + console.log('โœ… getDataContractHistory single view without proof confirmed'); + }); + + test('should execute getDataContract query with proof info', async () => { + const { result, proofEnabled } = await executeQueryWithProof( + wasmSdkPage, + parameterInjector, + 'dataContract', + 'getDataContract', + 'testnet' + ); + + // Validate basic result + validateBasicQueryResult(result); + validateContractResult(result.result); + + // If proof was enabled, verify split view + if (proofEnabled) { + validateSplitView(result); + console.log('โœ… getDataContract split view with proof confirmed'); + } else { + console.log('โš ๏ธ Proof was not enabled for getDataContract query'); + } + }); + + // Skip this test - proof support not yet implemented in WASM SDK for getDataContracts + test.skip('should execute getDataContracts query with proof info', async () => { + const { result, proofEnabled } = await executeQueryWithProof( + wasmSdkPage, + parameterInjector, + 'dataContract', + 'getDataContracts', + 'testnet' + ); + + // Validate basic result + validateBasicQueryResult(result); + + // Multiple contracts result should be valid JSON + expect(() => JSON.parse(result.result)).not.toThrow(); + const contractsData = JSON.parse(result.result); + expect(contractsData).toBeDefined(); + + // If proof was enabled, verify split view + if (proofEnabled) { + validateSplitView(result); + console.log('โœ… getDataContracts split view with proof confirmed'); + } else { + console.log('โš ๏ธ Proof was not enabled for getDataContracts query'); + } + }); + + // Skip this test - proof support not yet implemented in WASM SDK for getDataContractHistory + test.skip('should execute getDataContractHistory query with proof info', async () => { + const { result, proofEnabled } = await executeQueryWithProof( + wasmSdkPage, + parameterInjector, + 'dataContract', + 'getDataContractHistory', + 'testnet' + ); + + // Validate basic result + validateBasicQueryResult(result); + + // Contract history should be valid JSON + expect(() => JSON.parse(result.result)).not.toThrow(); + const historyData = JSON.parse(result.result); + expect(historyData).toBeDefined(); + expect(Array.isArray(historyData) || typeof historyData === 'object').toBe(true); + + // If proof was enabled, verify split view + if (proofEnabled) { + validateSplitView(result); + console.log('โœ… getDataContractHistory split view with proof confirmed'); + } else { + console.log('โš ๏ธ Proof was not enabled for getDataContractHistory query'); + } + }); + }); + + test.describe('Document Queries', () => { + test('should execute getDocuments query', async () => { + await wasmSdkPage.setupQuery('document', 'getDocuments'); + + const success = await parameterInjector.injectParameters('document', 'getDocuments', 'testnet'); + expect(success).toBe(true); + + const result = await wasmSdkPage.executeQueryAndGetResult(); + + // Use helper functions for validation + validateBasicQueryResult(result); + validateSingleView(result); + validateDocumentResult(result.result); + + console.log('โœ… getDocuments single view without proof confirmed'); + }); + + test('should execute getDocument query for specific document', async () => { + await wasmSdkPage.setupQuery('document', 'getDocument'); + + const success = await parameterInjector.injectParameters('document', 'getDocument', 'testnet'); + expect(success).toBe(true); + + const result = await wasmSdkPage.executeQueryAndGetResult(); + + // Use helper functions for validation + validateBasicQueryResult(result); + validateSingleView(result); + validateDocumentResult(result.result); + + console.log('โœ… getDocument single view without proof confirmed'); + }); + + test('should execute getDocuments query with proof info', async () => { + const { result, proofEnabled } = await executeQueryWithProof( + wasmSdkPage, + parameterInjector, + 'document', + 'getDocuments', + 'testnet' + ); + + // Validate basic result + validateBasicQueryResult(result); + validateDocumentResult(result.result); + + // If proof was enabled, verify split view + if (proofEnabled) { + validateSplitView(result); + console.log('โœ… getDocuments split view with proof confirmed'); + } else { + console.log('โš ๏ธ Proof was not enabled for getDocuments query'); + } + }); + + test('should execute getDocument query with proof info', async () => { + const { result, proofEnabled } = await executeQueryWithProof( + wasmSdkPage, + parameterInjector, + 'document', + 'getDocument', + 'testnet' + ); + + // Validate basic result + validateBasicQueryResult(result); + validateDocumentResult(result.result); + + // If proof was enabled, verify split view + if (proofEnabled) { + validateSplitView(result); + console.log('โœ… getDocument split view with proof confirmed'); + } else { + console.log('โš ๏ธ Proof was not enabled for getDocument query'); + } + }); + }); + + test.describe('System Queries', () => { + test('should execute getStatus query', async () => { + await wasmSdkPage.setupQuery('system', 'getStatus'); + + // Status query needs no parameters + const result = await wasmSdkPage.executeQueryAndGetResult(); + + // Status should generally succeed + expect(result.success).toBe(true); + expect(result.result).toBeDefined(); + expect(result.result).toContain('version'); + + }); + + test('should execute getCurrentEpoch query', async () => { + await wasmSdkPage.setupQuery('epoch', 'getCurrentEpoch'); + + const result = await wasmSdkPage.executeQueryAndGetResult(); + + // Verify query executed successfully + expect(result.success).toBe(true); + expect(result.result).toBeDefined(); + + // Verify the result is not an error message + expect(result.hasError).toBe(false); + expect(result.result).not.toContain('Error executing query'); + expect(result.result).not.toContain('not found'); + + // Should contain epoch data (number or JSON with epoch info) + expect(result.result).toMatch(/\d+|epoch/i); + + }); + + test('should execute getTotalCreditsInPlatform query', async () => { + await wasmSdkPage.setupQuery('system', 'getTotalCreditsInPlatform'); + + const result = await wasmSdkPage.executeQueryAndGetResult(); + + // Verify query executed successfully + expect(result.success).toBe(true); + expect(result.result).toBeDefined(); + + // Verify the result is not an error message + expect(result.hasError).toBe(false); + expect(result.result).not.toContain('Error executing query'); + expect(result.result).not.toContain('not found'); + + // Should contain credits data (number or JSON with credits info) + expect(result.result).toMatch(/\d+|credits|balance/i); + + }); + }); + + test.describe('Error Handling', () => { + test('should handle invalid identity ID gracefully', async () => { + await wasmSdkPage.setupQuery('identity', 'getIdentity'); + + // Fill with invalid ID (contains invalid base58 characters '0', 'O', 'I', 'l') + await wasmSdkPage.fillQueryParameters({ id: 'GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4SOIl0' }); + + // Click execute button directly + const executeButton = wasmSdkPage.page.locator('#executeQuery'); + await executeButton.click(); + + // Wait a bit for the error to appear + await wasmSdkPage.page.waitForTimeout(1000); + + // Check for error status + const statusBanner = wasmSdkPage.page.locator('#statusBanner'); + const statusClass = await statusBanner.getAttribute('class'); + const statusText = await wasmSdkPage.getStatusBannerText(); + + // Should show error + expect(statusClass).toContain('error'); + expect(statusText).toBeTruthy(); + + console.log('Error handling result:', statusText); + }); + + test('should handle empty required fields', async () => { + await wasmSdkPage.setupQuery('identity', 'getIdentity'); + + // Don't fill any parameters, try to execute + const executeButton = wasmSdkPage.page.locator('#executeQuery'); + await executeButton.click(); + + // Wait a bit for the error to appear + await wasmSdkPage.page.waitForTimeout(1000); + + // Check for error status + const statusBanner = wasmSdkPage.page.locator('#statusBanner'); + const statusClass = await statusBanner.getAttribute('class'); + const statusText = await wasmSdkPage.getStatusBannerText(); + + // Should show error or validation message + expect(statusClass).toContain('error'); + expect(statusText).toContain('required'); + + console.log('Empty fields handling:', statusText); + }); + }); + + + test.describe('Network Switching', () => { + test('should execute queries on mainnet', async () => { + // Switch to mainnet + await wasmSdkPage.setNetwork('mainnet'); + + await wasmSdkPage.setupQuery('system', 'getStatus'); + + const result = await wasmSdkPage.executeQueryAndGetResult(); + + // Verify query executed successfully + expect(result.success).toBe(true); + expect(result.result).toBeDefined(); + + // Verify the result is not an error message + expect(result.hasError).toBe(false); + expect(result.result).not.toContain('Error executing query'); + expect(result.result).not.toContain('not found'); + + // Should contain status data with version info + expect(result.result).toContain('version'); + + }); + }); + + // Test Identity Queries + test.describe('Identity Queries', () => { + // Complete set of all available identity queries with correct proof support + const testQueries = [ + { name: 'getIdentity', hasProofSupport: true, validateFn: validateIdentityResult }, + { name: 'getIdentityBalance', hasProofSupport: false, validateFn: (result) => validateNumericResult(result, 'balance') }, + { name: 'getIdentityKeys', hasProofSupport: false, validateFn: validateKeysResult }, + { name: 'getIdentityNonce', hasProofSupport: true, validateFn: (result) => validateNumericResult(result, 'nonce') }, + { name: 'getIdentityContractNonce', hasProofSupport: true, validateFn: (result) => validateNumericResult(result, 'nonce') }, + { name: 'getIdentityByPublicKeyHash', hasProofSupport: false, validateFn: validateIdentityResult }, + { name: 'getIdentitiesContractKeys', hasProofSupport: false, validateFn: validateKeysResult }, + { name: 'getIdentitiesBalances', hasProofSupport: false, validateFn: validateBalancesResult }, + { name: 'getIdentityBalanceAndRevision', hasProofSupport: false, validateFn: validateBalanceAndRevisionResult }, + { name: 'getIdentityByNonUniquePublicKeyHash', hasProofSupport: false, validateFn: validateIdentitiesResult }, + { name: 'getIdentityTokenBalances', hasProofSupport: false, validateFn: validateTokenBalanceResult }, + { name: 'getIdentitiesTokenBalances', hasProofSupport: true, validateFn: validateTokenBalanceResult }, + { name: 'getIdentityTokenInfos', hasProofSupport: false, validateFn: validateTokenInfoResult }, + { name: 'getIdentitiesTokenInfos', hasProofSupport: false, validateFn: validateTokenInfoResult } + ]; + + testQueries.forEach(({ name, hasProofSupport, validateFn }) => { + test.describe(`${name} query (parameterized)`, () => { + test('without proof info', async () => { + await wasmSdkPage.setupQuery('identity', name); + await wasmSdkPage.disableProofInfo(); + + const success = await parameterInjector.injectParameters('identity', name, 'testnet'); + expect(success).toBe(true); + + const result = await wasmSdkPage.executeQueryAndGetResult(); + validateBasicQueryResult(result); + expect(result.result.length).toBeGreaterThan(0); + validateSingleView(result); + validateFn(result.result); + + console.log(`โœ… ${name} without proof - PASSED`); + }); + + if (hasProofSupport) { + test('with proof info', async () => { + const { result, proofEnabled } = await executeQueryWithProof( + wasmSdkPage, + parameterInjector, + 'identity', + name, + 'testnet' + ); + + validateBasicQueryResult(result); + expect(result.result.length).toBeGreaterThan(0); + + if (proofEnabled) { + validateSplitView(result); + validateFn(result.result); + console.log(`โœ… ${name} with proof - PASSED`); + } else { + console.log(`โš ๏ธ Proof was not enabled for ${name} query`); + validateFn(result.result); + } + }); + } else { + test.skip('with proof info', async () => { + // Proof support not yet implemented in WASM SDK for this query + }); + } + }); + }); + }); +}); diff --git a/packages/wasm-sdk/test/ui-automation/utils/base-test.js b/packages/wasm-sdk/test/ui-automation/utils/base-test.js new file mode 100644 index 00000000000..9ed39f08d76 --- /dev/null +++ b/packages/wasm-sdk/test/ui-automation/utils/base-test.js @@ -0,0 +1,232 @@ +const { expect } = require('@playwright/test'); +const fs = require('fs'); + +/** + * Base test utilities for WASM SDK UI automation + */ +class BaseTest { + constructor(page) { + this.page = page; + } + + /** + * Navigate to the WASM SDK index page and wait for initialization + */ + async navigateToSdk() { + await this.page.goto('/'); + + // Wait for the WASM SDK to initialize + await this.page.waitForSelector('#statusBanner.success', { + timeout: 60000, + state: 'visible' + }); + + // Verify we're on the right page + await expect(this.page).toHaveTitle(/Dash Platform WASM JS SDK/); + + console.log('SDK initialized successfully'); + } + + /** + * Wait for SDK to be in success state (useful after network changes) + */ + async waitForSdkReady() { + await this.page.waitForSelector('#statusBanner.success', { + timeout: 30000, + state: 'visible' + }); + + // Additional wait to ensure stability + await this.page.waitForTimeout(500); + } + + /** + * Wait for network loading to complete + */ + async waitForNetworkIdle() { + await this.page.waitForLoadState('networkidle'); + } + + /** + * Take a screenshot with a descriptive name + */ + async takeScreenshot(name) { + const screenshotDir = 'test-results/screenshots/'; + + // Create directory if it doesn't exist + if (!fs.existsSync(screenshotDir)) { + fs.mkdirSync(screenshotDir, { recursive: true }); + } + + await this.page.screenshot({ + path: `${screenshotDir}${name}-${Date.now()}.png`, + fullPage: true + }); + } + + /** + * Wait for an element to be visible and ready for interaction + */ + async waitForElement(selector, options = {}) { + const element = this.page.locator(selector); + await element.waitFor({ state: 'visible', ...options }); + return element; + } + + /** + * Fill input field with validation + */ + async fillInput(selector, value, options = {}) { + const input = await this.waitForElement(selector); + await input.clear(); + await input.fill(value); + + // Verify the value was set correctly + if (options.verify !== false) { + await expect(input).toHaveValue(value); + } + + return input; + } + + /** + * Select option from dropdown + */ + async selectOption(selector, value) { + const select = await this.waitForElement(selector); + await select.selectOption(value); + + // Verify selection + await expect(select).toHaveValue(value); + + return select; + } + + /** + * Click button and wait for any loading states + */ + async clickButton(selector, options = {}) { + const button = await this.waitForElement(selector); + + // Check if button is enabled + await expect(button).toBeEnabled(); + + // Click and optionally wait for response + await button.click(); + + if (options.waitForResponse) { + await this.page.waitForResponse(response => + response.url().includes('dapi') + ); + } + + return button; + } + + /** + * Get the current result content + */ + async getResultContent() { + const resultContainer = this.page.locator('#identityInfo'); + await resultContainer.waitFor({ state: 'visible' }); + return await resultContainer.textContent(); + } + + /** + * Check if result shows an error + */ + async hasErrorResult() { + const resultContainer = this.page.locator('#identityInfo'); + const classList = await resultContainer.getAttribute('class'); + return classList && classList.includes('error'); + } + + /** + * Clear results + */ + async clearResults() { + await this.clickButton('#clearButton'); + const resultContainer = this.page.locator('#identityInfo'); + await expect(resultContainer).toHaveClass(/empty/); + } + + /** + * Set network (mainnet/testnet) + */ + async setNetwork(network = 'testnet') { + const networkRadio = this.page.locator(`#${network}`); + await networkRadio.check(); + + // Wait for network indicator to update + const indicator = this.page.locator('#networkIndicator'); + await expect(indicator).toContainText(network.toUpperCase()); + + // Network changes might trigger SDK re-initialization, so wait a bit + await this.page.waitForTimeout(1000); + + console.log(`Network set to ${network}`); + } + + /** + * Set operation type (queries, transitions, wallet) + */ + async setOperationType(type = 'queries') { + await this.selectOption('#operationType', type); + console.log(`Operation type set to ${type}`); + } + + /** + * Set query category + */ + async setQueryCategory(category) { + await this.selectOption('#queryCategory', category); + + // Wait for query type dropdown to populate + await this.page.waitForTimeout(500); + + console.log(`Query category set to ${category}`); + } + + /** + * Set specific query type + */ + async setQueryType(queryType) { + // Make sure query type dropdown is visible + await this.waitForElement('#queryType'); + await this.selectOption('#queryType', queryType); + + // Wait for inputs to appear + await this.page.waitForTimeout(500); + + console.log(`Query type set to ${queryType}`); + } + + /** + * Execute the current query and wait for results + */ + async executeQuery() { + const executeButton = this.page.locator('#executeQuery'); + + // Ensure button is visible and enabled + await expect(executeButton).toBeVisible(); + await expect(executeButton).toBeEnabled(); + + // Click execute button + await executeButton.click(); + + // Wait for status banner to show loading + await this.page.locator('#statusBanner.loading').waitFor({ state: 'visible' }); + + // Wait for loading to complete (either success or error) + await this.page.locator('#statusBanner.loading').waitFor({ state: 'hidden', timeout: 30000 }); + + console.log('Query executed'); + + // Return whether it was successful + const statusBanner = this.page.locator('#statusBanner'); + const statusClass = await statusBanner.getAttribute('class'); + return statusClass && statusClass.includes('success'); + } +} + +module.exports = { BaseTest }; diff --git a/packages/wasm-sdk/test/ui-automation/utils/parameter-injector.js b/packages/wasm-sdk/test/ui-automation/utils/parameter-injector.js new file mode 100644 index 00000000000..aa1f555c85e --- /dev/null +++ b/packages/wasm-sdk/test/ui-automation/utils/parameter-injector.js @@ -0,0 +1,267 @@ +const { getTestParameters, getAllTestParameters } = require('../fixtures/test-data'); + +/** + * Parameter injection system for WASM SDK UI tests + * Maps test data to UI form fields automatically + */ +class ParameterInjector { + constructor(wasmSdkPage) { + this.page = wasmSdkPage; + } + + /** + * Inject parameters for a specific query based on test data + */ + async injectParameters(category, queryType, network = 'testnet', parameterSetIndex = 0) { + try { + const allParameters = getAllTestParameters(category, queryType, network); + + if (allParameters.length === 0) { + console.warn(`โš ๏ธ No test parameters found for ${category}.${queryType} on ${network}`); + return false; + } + + const parameters = allParameters[parameterSetIndex] || allParameters[0]; + console.log(`๐Ÿ“ Injecting parameters for ${category}.${queryType}:`, parameters); + + await this.page.fillQueryParameters(parameters); + return true; + } catch (error) { + console.error(`โŒ Failed to inject parameters for ${category}.${queryType}:`, error.message); + return false; + } + } + + /** + * Get parameter mapping for manual field filling + * Maps parameter names to likely field selectors + */ + getParameterFieldMapping() { + return { + // Identity parameters + 'id': ['#id', '[name="id"]', 'input[placeholder*="Identity ID"]'], + 'identityId': ['#identityId', '[name="identityId"]', 'input[placeholder*="Identity ID"]'], + 'identityIds': ['input[placeholder="Enter value"]', '.array-input-container input[type="text"]', '[data-array-name="identityIds"] input[type="text"]', '.array-input-container[data-array-name="identityIds"] input', '#identityIds', '[name="identityIds"]', 'input[placeholder*="Identity IDs"]'], + 'identitiesIds': ['input[placeholder="Enter value"]', '.array-input-container input[type="text"]', '[data-array-name="identitiesIds"] input[type="text"]', '.array-input-container[data-array-name="identitiesIds"] input', '#identitiesIds', '[name="identitiesIds"]', 'input[placeholder*="Identity IDs"]'], + + // Data contract parameters + 'dataContractId': ['#dataContractId', '[name="dataContractId"]', 'input[placeholder*="Contract ID"]'], + 'contractId': ['#contractId', '[name="contractId"]', 'input[placeholder*="Contract ID"]'], + 'ids': ['input[placeholder="Enter value"]', '.array-input-container input[type="text"]', '[data-array-name="ids"] input[type="text"]', '.array-input-container[data-array-name="ids"] input', '#ids', '[name="ids"]', 'input[placeholder*="Contract IDs"]', 'input[placeholder*="Data Contract ID"]'], + + // Document parameters + 'documentType': ['#documentType', '[name="documentType"]', 'input[placeholder*="Document Type"]'], + 'documentId': ['#documentId', '[name="documentId"]', 'input[placeholder*="Document ID"]'], + + // Key parameters + 'publicKeyHash': ['#publicKeyHash', '[name="publicKeyHash"]', 'input[placeholder*="Public Key Hash"]'], + 'keyRequestType': ['#keyRequestType', '[name="keyRequestType"]', 'select[name="keyRequestType"]'], + 'specificKeyIds': ['#specificKeyIds', '[name="specificKeyIds"]'], + + // Token parameters + 'tokenId': ['#tokenId', '[name="tokenId"]', 'input[placeholder*="Token ID"]'], + 'tokenIds': ['input[placeholder="Enter value"]', '.array-input-container input[type="text"]', '[data-array-name="tokenIds"] input[type="text"]', '.array-input-container[data-array-name="tokenIds"] input', '#tokenIds', '[name="tokenIds"]', 'input[placeholder*="Token IDs"]'], + + // Query modifiers + 'limit': ['#limit', '[name="limit"]', 'input[placeholder*="limit" i]'], + 'offset': ['#offset', '[name="offset"]', 'input[placeholder*="offset" i]'], + 'count': ['#count', '[name="count"]', 'input[placeholder*="count" i]'], + + // Epoch parameters + 'epoch': ['#epoch', '[name="epoch"]', 'input[placeholder*="epoch" i]'], + 'startEpoch': ['#startEpoch', '[name="startEpoch"]'], + 'ascending': ['#ascending', '[name="ascending"]', 'input[type="checkbox"][name="ascending"]'], + + // ProTx parameters + 'startProTxHash': ['#startProTxHash', '[name="startProTxHash"]'], + 'proTxHashes': ['#proTxHashes', '[name="proTxHashes"]'], + + // Where clause and ordering + 'whereClause': ['#whereClause', '[name="whereClause"]', 'textarea[placeholder*="Where"]'], + 'orderBy': ['#orderBy', '[name="orderBy"]', 'textarea[placeholder*="Order"]'], + + // Voting parameters + 'documentTypeName': ['#documentTypeName', '[name="documentTypeName"]'], + 'indexName': ['#indexName', '[name="indexName"]'], + 'resultType': ['#resultType', '[name="resultType"]'], + 'contestantId': ['#contestantId', '[name="contestantId"]'], + + // Time parameters + 'startTimeMs': ['#startTimeMs', '[name="startTimeMs"]'], + 'endTimeMs': ['#endTimeMs', '[name="endTimeMs"]'] + }; + } + + /** + * Auto-detect and fill parameters using intelligent field matching + */ + async autoFillParameters(parameters) { + const fieldMapping = this.getParameterFieldMapping(); + const filledFields = []; + const failedFields = []; + + for (const [paramName, value] of Object.entries(parameters)) { + const success = await this.tryFillParameter(paramName, value, fieldMapping); + + if (success) { + filledFields.push(paramName); + } else { + failedFields.push(paramName); + } + } + + console.log(`Successfully filled fields: ${filledFields.join(', ')}`); + if (failedFields.length > 0) { + console.warn(`โš ๏ธ Failed to fill fields: ${failedFields.join(', ')}`); + } + + return { filledFields, failedFields }; + } + + /** + * Try to fill a parameter using multiple selector strategies + */ + async tryFillParameter(paramName, value, fieldMapping) { + const possibleSelectors = fieldMapping[paramName] || []; + + // Add generic fallback selectors + possibleSelectors.push( + `#${paramName}`, + `[name="${paramName}"]`, + `input[placeholder*="${paramName}" i]`, + `label:has-text("${paramName}") + input`, + `label:has-text("${paramName}") + select`, + `label:has-text("${paramName}") + textarea` + ); + + for (const selector of possibleSelectors) { + try { + const element = this.page.page.locator(selector).first(); + const count = await element.count(); + + if (count > 0) { + const isVisible = await element.isVisible(); + + if (isVisible) { + await this.page.fillInputByType(element, value); + console.log(`๐Ÿ“ Filled ${paramName} using selector: ${selector}`); + return true; + } + } + } catch (error) { + continue; + } + } + + return false; + } + + /** + * Create test parameter sets for parameterized testing + */ + createParameterizedTests(category, queryType, network = 'testnet') { + const allParameters = getAllTestParameters(category, queryType, network); + + return allParameters.map((params, index) => ({ + testName: `${category}.${queryType} - Test Set ${index + 1}`, + parameters: params, + category, + queryType, + network, + index + })); + } + + /** + * Validate parameters against expected schema + */ + validateParameters(parameters, expectedSchema = {}) { + const validation = { + valid: true, + errors: [], + warnings: [] + }; + + for (const [key, value] of Object.entries(parameters)) { + // Check for empty required values + if (value === null || value === undefined || value === '') { + validation.warnings.push(`Parameter '${key}' is empty`); + } + + // Validate array parameters + if (Array.isArray(value) && value.length === 0) { + validation.warnings.push(`Array parameter '${key}' is empty`); + } + + // Validate ID format (base58 compliance for Dash IDs) + if (key.toLowerCase().includes('id') && typeof value === 'string') { + if (!this.isValidBase58DashId(value)) { + validation.errors.push(`Parameter '${key}' is not a valid base58-encoded Dash ID: ${value}`); + validation.valid = false; + } + } + } + + return validation; + } + + /** + * Validate if a string is a valid base58-encoded Dash ID + * @param {string} id - The ID string to validate + * @returns {boolean} - true if valid base58 Dash ID format + */ + isValidBase58DashId(id) { + // Dash IDs are typically 44 characters long when base58 encoded + // Base58 alphabet: 123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz + // (excludes 0, O, I, l to avoid confusion) + const base58Regex = /^[1-9A-HJ-NP-Za-km-z]{43,44}$/; + + if (!base58Regex.test(id)) { + return false; + } + + // Additional check: ensure it doesn't contain invalid base58 characters + const invalidChars = /[0OIl]/; + if (invalidChars.test(id)) { + return false; + } + + return true; + } + + /** + * Generate random test parameters for stress testing + */ + generateRandomParameters(category, queryType) { + // This would generate valid-looking but random parameters + // for stress testing the UI with various inputs + const generators = { + id: () => 'GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec', // Use known good ID + identityId: () => 'GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec', + dataContractId: () => 'GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec', + limit: () => Math.floor(Math.random() * 100) + 1, + count: () => Math.floor(Math.random() * 50) + 1, + epoch: () => Math.floor(Math.random() * 10000) + 1000, + ascending: () => Math.random() > 0.5 + }; + + // Get base parameters and randomize some values + try { + const baseParams = getTestParameters(category, queryType, 'testnet'); + const randomized = { ...baseParams }; + + for (const [key] of Object.entries(randomized)) { + if (generators[key]) { + randomized[key] = generators[key](); + } + } + + return randomized; + } catch (error) { + console.warn(`Could not generate random parameters for ${category}.${queryType}`); + return {}; + } + } +} + +module.exports = { ParameterInjector }; diff --git a/packages/wasm-sdk/test/ui-automation/utils/wasm-sdk-page.js b/packages/wasm-sdk/test/ui-automation/utils/wasm-sdk-page.js new file mode 100644 index 00000000000..de2fa8ef93b --- /dev/null +++ b/packages/wasm-sdk/test/ui-automation/utils/wasm-sdk-page.js @@ -0,0 +1,520 @@ +const { BaseTest } = require('./base-test'); +const { expect } = require('@playwright/test'); + +// Configuration for dynamic array parameters that require special handling +const DYNAMIC_ARRAY_PARAMETERS = { + 'ids': true, + 'identityIds': true, + 'identitiesIds': true, + 'tokenIds': true +}; + +/** + * Page Object Model for WASM SDK index.html interface + */ +class WasmSdkPage extends BaseTest { + constructor(page) { + super(page); + + // Define selectors for all interface elements + this.selectors = { + // Status and initialization + statusBanner: '#statusBanner', + statusBannerSuccess: '#statusBanner.success', + statusBannerLoading: '#statusBanner.loading', + statusBannerError: '#statusBanner.error', + + // Network controls + mainnetRadio: '#mainnet', + testnetRadio: '#testnet', + networkIndicator: '#networkIndicator', + trustedModeCheckbox: '#trustedMode', + + // Operation selectors + operationType: '#operationType', + queryCategory: '#queryCategory', + queryType: '#queryType', + + // Query inputs + queryInputs: '#queryInputs', + queryTitle: '#queryTitle', + dynamicInputs: '#dynamicInputs', + + // Authentication inputs + authenticationInputs: '#authenticationInputs', + identityId: '#identityId', + privateKey: '#privateKey', + assetLockProof: '#assetLockProof', + + // Proof toggle + proofToggleContainer: '#proofToggleContainer', + proofToggle: '#proofToggle', + + // Execute button + executeQuery: '#executeQuery', + + // Results + resultContainer: '.result-container', + resultContent: '#identityInfo', + resultHeader: '.result-header', + + // Action buttons + clearButton: '#clearButton', + copyButton: '#copyButton', + clearCacheButton: '#clearCacheButton', + + // Advanced SDK configuration + sdkConfigDetails: '.sdk-config', + platformVersion: '#platformVersion', + connectTimeout: '#connectTimeout', + requestTimeout: '#requestTimeout', + retries: '#retries', + banFailedAddress: '#banFailedAddress', + applyConfig: '#applyConfig' + }; + } + + /** + * Initialize the SDK page + */ + async initialize(network = 'testnet') { + await this.navigateToSdk(); + await this.setNetwork(network); + + // Wait for SDK to be ready after network change + await this.waitForSdkReady(); + + return this; + } + + /** + * Set up a query test scenario + */ + async setupQuery(category, queryType, parameters = {}) { + // Set operation type to queries + await this.setOperationType('queries'); + + // Set category and query type + await this.setQueryCategory(category); + await this.setQueryType(queryType); + + // Fill in parameters + if (Object.keys(parameters).length > 0) { + await this.fillQueryParameters(parameters); + } + + return this; + } + + /** + * Fill query parameters dynamically based on the input structure + */ + async fillQueryParameters(parameters) { + for (const [key, value] of Object.entries(parameters)) { + await this.fillParameterByName(key, value); + } + } + + /** + * Fill a specific parameter by name + */ + async fillParameterByName(paramName, value) { + // Special handling for array parameters that use dynamic input fields + if (DYNAMIC_ARRAY_PARAMETERS[paramName]) { + const enterValueInput = this.page.locator('input[placeholder="Enter value"]').first(); + const count = await enterValueInput.count(); + + if (count > 0 && await enterValueInput.isVisible()) { + await this.fillInputByType(enterValueInput, value); + return; + } + } + + const inputSelector = `input[name="${paramName}"], select[name="${paramName}"], textarea[name="${paramName}"]`; + const input = this.page.locator(inputSelector).first(); + + // Check if input exists + if (await input.count() === 0) { + // Try alternative selectors based on common patterns + const alternativeSelectors = [ + `#${paramName}`, + `[id*="${paramName}"]`, + `[placeholder*="${paramName}"]`, + `label:has-text("${paramName}") + input`, + `label:has-text("${paramName}") + select`, + `label:has-text("${paramName}") + textarea` + ]; + + let found = false; + for (const selector of alternativeSelectors) { + const altInput = this.page.locator(selector).first(); + if (await altInput.count() > 0) { + await this.fillInputByType(altInput, value); + found = true; + break; + } + } + + if (!found) { + console.warn(`โš ๏ธ Could not find input for parameter: ${paramName}`); + } + } else { + await this.fillInputByType(input, value); + } + } + + /** + * Fill input based on its type + */ + async fillInputByType(inputElement, value) { + const tagName = await inputElement.evaluate(el => el.tagName.toLowerCase()); + const inputType = await inputElement.evaluate(el => el.type); + + if (tagName === 'select') { + await inputElement.selectOption(value.toString()); + } else if (inputType === 'checkbox') { + if (value) { + await inputElement.check(); + } else { + await inputElement.uncheck(); + } + } else if (Array.isArray(value)) { + // Handle array inputs - check if there's an "Add items" button nearby + const success = await this.handleArrayInput(inputElement, value); + if (!success) { + // Fallback to JSON string if array handling fails + await inputElement.fill(JSON.stringify(value)); + } + } else if (typeof value === 'object') { + // Handle object inputs (JSON) + await inputElement.fill(JSON.stringify(value)); + } else { + // Handle text/number inputs + await inputElement.fill(value.toString()); + } + } + + /** + * Handle array inputs with "Add items" button functionality + */ + async handleArrayInput(baseElement, arrayValues) { + try { + // Look for existing input fields first (prioritize array container inputs) + const arrayContainerInputs = this.page.locator('.array-input-container input[type="text"]'); + const allInputs = this.page.locator('input[type="text"], textarea').filter({ + hasNot: this.page.locator('[readonly]') + }); + + // Use array container inputs if available, otherwise use all inputs + const existingInputs = await arrayContainerInputs.count() > 0 ? arrayContainerInputs : allInputs; + const existingCount = await existingInputs.count(); + + // Fill the first existing field if available + if (existingCount > 0 && arrayValues.length > 0) { + const firstInput = existingInputs.first(); + await firstInput.fill(arrayValues[0].toString()); + } + + // Look for "Add Item" button (specific to WASM SDK array inputs) + const addButton = this.page.locator('button:has-text("+ Add Item"), button.add-array-item, button:has-text("Add Item"), button:has-text("Add"), button:has-text("add")').first(); + + if (await addButton.count() === 0) { + if (arrayValues.length <= 1) { + return true; + } else { + return false; + } + } + + // Add remaining items (starting from index 1) + for (let i = 1; i < arrayValues.length; i++) { + const value = arrayValues[i]; + + // Click "Add items" button to create new field + await addButton.click(); + await this.page.waitForTimeout(500); // Wait for new input to appear + + // Find all input fields again (should be one more now) + const currentArrayInputs = this.page.locator('.array-input-container input[type="text"]'); + const currentAllInputs = this.page.locator('input[type="text"], textarea').filter({ + hasNot: this.page.locator('[readonly]') + }); + + // Use array container inputs if available + const currentInputs = await currentArrayInputs.count() > 0 ? currentArrayInputs : currentAllInputs; + const currentCount = await currentInputs.count(); + + if (currentCount > existingCount + (i - 1)) { + // Fill the newest input field + const newInput = currentInputs.nth(currentCount - 1); + await newInput.fill(value.toString()); + } else { + console.warn(`Could not find new input field for item ${i + 1}`); + } + } + + return true; + } catch (error) { + console.warn(`Array input handling failed: ${error.message}`); + return false; + } + } + + /** + * Helper method to toggle proof information + * @param {boolean} enable - true to enable, false to disable + * @returns {boolean} - true if successful, false if proof toggle not available + */ + async _toggleProofInfo(enable) { + // Wait a moment for the UI to fully load after query setup + await this.page.waitForTimeout(1000); + + const proofContainer = this.page.locator(this.selectors.proofToggleContainer); + + // Check if proof container exists and becomes visible + try { + // Wait longer and check if container becomes visible or is already attached + await proofContainer.waitFor({ state: 'attached', timeout: 10000 }); + + // Check if it's visible or can be made visible + const isVisible = await proofContainer.isVisible(); + if (!isVisible) { + // It might be hidden by display:none, check if it exists in the DOM + const count = await proofContainer.count(); + if (count === 0) { + console.log('โš ๏ธ Proof toggle container not found in DOM'); + return false; + } + + // Try to wait a bit more for it to become visible + try { + await proofContainer.waitFor({ state: 'visible', timeout: 3000 }); + } catch { + console.log('โš ๏ธ Proof toggle container exists but remains hidden - may not be available for this query type'); + return false; + } + } + + const proofToggle = this.page.locator(this.selectors.proofToggle); + + // Check current state and toggle if needed + const isChecked = await proofToggle.isChecked(); + const needsToggle = enable ? !isChecked : isChecked; + + if (needsToggle) { + // Click on the toggle switch container or label to toggle it + // Since it's a custom toggle, we need to click the label or toggle-slider + const toggleLabel = proofContainer.locator('label'); + await toggleLabel.click(); + + // Wait for the toggle to reach the expected state + if (enable) { + await expect(proofToggle).toBeChecked(); + console.log('Proof toggle confirmed as checked'); + } else { + await expect(proofToggle).not.toBeChecked(); + console.log('Proof toggle confirmed as unchecked'); + } + } + + console.log(`Proof info ${enable ? 'enabled' : 'disabled'}`); + return true; + } catch (error) { + console.log(`โš ๏ธ Proof toggle not available for this query type: ${error.message}`); + return false; + } + } + + /** + * Enable proof information toggle + */ + async enableProofInfo() { + return this._toggleProofInfo(true); + } + + /** + * Disable proof information toggle + */ + async disableProofInfo() { + return this._toggleProofInfo(false); + } + + /** + * Get the query description text + */ + async getQueryDescription() { + const description = this.page.locator('#queryDescription'); + if (await description.count() > 0) { + return await description.textContent(); + } + return null; + } + + /** + * Check if authentication inputs are visible + */ + async hasAuthenticationInputs() { + const authInputs = this.page.locator(this.selectors.authenticationInputs); + return await authInputs.isVisible(); + } + + /** + * Fill authentication information + */ + async fillAuthentication(identityId, privateKey, assetLockProof = null) { + if (await this.hasAuthenticationInputs()) { + if (identityId) { + await this.fillInput(this.selectors.identityId, identityId); + } + if (privateKey) { + await this.fillInput(this.selectors.privateKey, privateKey); + } + if (assetLockProof) { + await this.fillInput(this.selectors.assetLockProof, assetLockProof); + } + console.log('Authentication filled'); + } + } + + /** + * Get current status banner state + */ + async getStatusBannerState() { + const banner = this.page.locator(this.selectors.statusBanner); + const classList = await banner.getAttribute('class'); + + // Handle null classList gracefully + if (!classList) return 'unknown'; + + if (classList.includes('success')) return 'success'; + if (classList.includes('error')) return 'error'; + if (classList.includes('loading')) return 'loading'; + return 'unknown'; + } + + /** + * Get status banner text + */ + async getStatusBannerText() { + const banner = this.page.locator(this.selectors.statusBanner); + return await banner.textContent(); + } + + /** + * Get proof content when in split view mode + */ + async getProofContent() { + await this.page.waitForTimeout(500); // Brief wait for content to render + + const proofContent = this.page.locator('#proofInfo'); + const isVisible = await proofContent.isVisible(); + + if (!isVisible) { + console.log('โš ๏ธ Proof content not visible'); + return ''; + } + + const content = await proofContent.textContent(); + return content || ''; + } + + /** + * Check if result is displayed in split view (proof mode) + */ + async isInSplitView() { + const dataSection = this.page.locator('.result-data-section'); + const proofSection = this.page.locator('.result-proof-section'); + + const dataSectionVisible = await dataSection.isVisible(); + const proofSectionVisible = await proofSection.isVisible(); + + return dataSectionVisible && proofSectionVisible; + } + + /** + * Wait for query execution to complete and return the result + */ + async executeQueryAndGetResult() { + const success = await this.executeQuery(); + const result = await this.getResultContent(); + const hasError = await this.hasErrorResult(); + + // Check if we're in split view mode (proof mode) + const inSplitView = await this.isInSplitView(); + let proofContent = null; + + if (inSplitView) { + proofContent = await this.getProofContent(); + } + + return { + success, + result, + hasError, + statusText: await this.getStatusBannerText(), + inSplitView, + proofContent + }; + } + + /** + * Configure advanced SDK settings + */ + async configureAdvancedSDK(options) { + // Open SDK config if it's closed + const configDetails = this.page.locator(this.selectors.sdkConfigDetails); + const isOpen = await configDetails.getAttribute('open') !== null; + + if (!isOpen) { + await configDetails.locator('summary').click(); + } + + // Fill configuration options + if (options.platformVersion) { + await this.fillInput(this.selectors.platformVersion, options.platformVersion); + } + if (options.connectTimeout) { + await this.fillInput(this.selectors.connectTimeout, options.connectTimeout); + } + if (options.requestTimeout) { + await this.fillInput(this.selectors.requestTimeout, options.requestTimeout); + } + if (options.retries) { + await this.fillInput(this.selectors.retries, options.retries); + } + if (options.banFailedAddress !== undefined) { + const checkbox = this.page.locator(this.selectors.banFailedAddress); + if (options.banFailedAddress) { + await checkbox.check(); + } else { + await checkbox.uncheck(); + } + } + + // Apply configuration + await this.clickButton(this.selectors.applyConfig); + + console.log('Advanced SDK configuration applied'); + } + + /** + * Get available query categories + */ + async getAvailableQueryCategories() { + const categorySelect = this.page.locator(this.selectors.queryCategory); + const options = await categorySelect.locator('option').allTextContents(); + return options.filter(option => option.trim() !== '' && option !== 'Select Query Category'); + } + + /** + * Get available query types for current category + */ + async getAvailableQueryTypes() { + const queryTypeSelect = this.page.locator(this.selectors.queryType); + await queryTypeSelect.waitFor({ state: 'visible' }); + const options = await queryTypeSelect.locator('option').allTextContents(); + return options.filter(option => option.trim() !== '' && option !== 'Select Query Type'); + } +} + +module.exports = { WasmSdkPage };