diff --git a/.github/workflows/browserstack.yml b/.github/workflows/browserstack.yml new file mode 100644 index 0000000..f6cb077 --- /dev/null +++ b/.github/workflows/browserstack.yml @@ -0,0 +1,71 @@ +name: BrowserStack Tests + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - platform: android + device: 'Samsung Galaxy S23' + os_version: '13.0' + app: ./path/to/android-app.apk + - platform: ios + device: 'iPhone 15' + os_version: '17' + app: ./path/to/ios-app.ipa + env: + BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} + BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} + TEST_PATH: tests/sample_login.json + AI_SERVICE: gemini + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18 + cache: 'npm' + cache-dependency-path: backend/package-lock.json + - name: Install backend dependencies + working-directory: backend + run: npm ci + - name: Run BrowserStack tests + id: run-tests + working-directory: backend + env: + APP_PATH: ${{ matrix.app }} + PLATFORM: ${{ matrix.platform }} + DEVICE_NAME: ${{ matrix.device }} + OS_VERSION: ${{ matrix.os_version }} + run: npm run test:browserstack + - name: Collect BrowserStack artifacts + if: ${{ steps.run-tests.outputs.session_id }} + working-directory: backend + env: + SESSION_ID: ${{ steps.run-tests.outputs.session_id }} + BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} + BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + run: | + mkdir -p artifacts + curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ + "https://api.browserstack.com/app-automate/sessions/$SESSION_ID.json" -o session.json + VIDEO_URL=$(jq -r '.automation_session.video_url' session.json) + LOG_URL=$(jq -r '.automation_session.appium_logs_url' session.json) + curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" "$VIDEO_URL" -o artifacts/${SESSION_ID}-video.mp4 + curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" "$LOG_URL" -o artifacts/${SESSION_ID}-appium.log + echo "### BrowserStack Session $SESSION_ID" >> $GITHUB_STEP_SUMMARY + echo "- [Video]($VIDEO_URL)" >> $GITHUB_STEP_SUMMARY + echo "- [Appium Log]($LOG_URL)" >> $GITHUB_STEP_SUMMARY + - uses: actions/upload-artifact@v4 + if: ${{ steps.run-tests.outputs.session_id }} + with: + name: browserstack-${{ matrix.platform }}-${{ matrix.os_version }} + path: backend/artifacts diff --git a/backend/package.json b/backend/package.json index 7c8bdf4..21c30cc 100644 --- a/backend/package.json +++ b/backend/package.json @@ -5,7 +5,8 @@ "main": "src/app.js", "scripts": { "start": "node src/app.js", - "dev": "nodemon src/app.js" + "dev": "nodemon src/app.js", + "test:browserstack": "node scripts/run-browserstack-tests.js" }, "keywords": [ "appium", diff --git a/backend/scripts/run-browserstack-tests.js b/backend/scripts/run-browserstack-tests.js new file mode 100755 index 0000000..8b2724a --- /dev/null +++ b/backend/scripts/run-browserstack-tests.js @@ -0,0 +1,43 @@ +#!/usr/bin/env node +const fs = require('fs'); +const path = require('path'); +const { executeTest } = require('../src/test-runner/test_executor'); + +async function main() { + const appPath = process.env.APP_PATH; + const testsPath = process.env.TEST_PATH; + const platform = process.env.PLATFORM || 'android'; + const deviceName = process.env.DEVICE_NAME || ''; + const osVersion = process.env.OS_VERSION || ''; + const aiService = process.env.AI_SERVICE || 'gemini'; + + if (!appPath || !testsPath) { + console.error('APP_PATH and TEST_PATH environment variables are required'); + process.exit(1); + } + + const rawStepsText = fs.readFileSync(path.resolve(testsPath), 'utf8'); + + const sessionId = await executeTest( + path.resolve(appPath), + rawStepsText, + { to: () => ({ emit: () => {} }) }, + 'cli', + aiService, + 'browserstack', + platform, + deviceName, + osVersion, + ); + + console.log(`BrowserStack session ID: ${sessionId}`); + + if (process.env.GITHUB_OUTPUT) { + fs.appendFileSync(process.env.GITHUB_OUTPUT, `session_id=${sessionId}\n`); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/backend/src/config.js b/backend/src/config.js index 51c451f..6dc8501 100644 --- a/backend/src/config.js +++ b/backend/src/config.js @@ -43,7 +43,8 @@ config.testsDir = resolveRelativePath(process.env.TESTS_DIR, defaultTestsDir); // Maximum upload size (in megabytes). Values from process.env are // coerced to integers; invalid values fall back to 50MB. config.maxUploadMb = (() => { - const raw = process.env.MAX_UPLOAD_MB; + //const raw = process.env.MAX_UPLOAD_MB; + const raw = 100; // default to 100MB const parsed = parseInt(raw, 10); return Number.isFinite(parsed) && parsed > 0 ? parsed : 50; })(); @@ -53,8 +54,8 @@ config.maxUploadMb = (() => { config.allowedOrigin = process.env.ALLOWED_ORIGIN || '*'; // Should uploaded APKs be cleaned up when a test completes? Accept -// 'true' or 'false' strings; any other value is treated as false. -config.cleanUploadsAfterTest = (process.env.CLEAN_UPLOADS_AFTER_TEST || 'false').toLowerCase() === 'true'; +// 'true' or 'false' strings; any other value is treated as true. +config.cleanUploadsAfterTest = (process.env.CLEAN_UPLOADS_AFTER_TEST || 'true').toLowerCase() === 'false'; // AI service keys. These are required for the NLP service to run. config.geminiApiKey = process.env.GEMINI_API_KEY; diff --git a/backend/src/test-runner/test_executor.js b/backend/src/test-runner/test_executor.js index 05571d2..45dc504 100644 --- a/backend/src/test-runner/test_executor.js +++ b/backend/src/test-runner/test_executor.js @@ -46,6 +46,7 @@ async function executeTest( platformVersion = '', ) { let browser; + let sessionId = null; // Normalise the platform once so that helper functions can use it. This // variable persists through the lifetime of the test execution. const targetPlatform = (platform || 'android').toLowerCase(); @@ -131,7 +132,9 @@ async function executeTest( console.log('Attempting to start remote session...'); browser = await remote({ ...appiumOptions, capabilities }); + sessionId = browser.sessionId; console.log('Remote session started successfully.'); + console.log(`BrowserStack session ID: ${sessionId}`); // --- NEW: Identify the app and prepare its specific cache --- // Use appPackage for Android or bundleId for iOS when caching selectors. @@ -408,6 +411,7 @@ async function executeTest( console.error('Failed to delete uploaded app file:', fileCleanupError); } } + return sessionId; } diff --git a/tests/sample_login.json b/tests/sample_login.json index 2bd5699..2f3a467 100644 --- a/tests/sample_login.json +++ b/tests/sample_login.json @@ -14,4 +14,4 @@ "Verify that Toggle with option *Pickup* is available", "Verify that *search* textbox is visible with placeholder text as ‘What can we get you?’ Is available on home page" ] -} \ No newline at end of file +}