diff --git a/.github/workflows/quick-testing.yml b/.github/workflows/quick-testing.yml index d1eb9e3c..cf35967a 100644 --- a/.github/workflows/quick-testing.yml +++ b/.github/workflows/quick-testing.yml @@ -14,17 +14,18 @@ jobs: matrix: os: [ubuntu-latest] architecture: [x64, arm64] - steps: - name: Checkout code uses: actions/checkout@v2 - + + - name: Install Docker Compose + run: | + sudo apt-get update + sudo apt-get install -y docker-compose + docker-compose --version + - name: Set up opensearch directory - run: mkdir shuffle-database - continue-on-error: true - - - name: Elevate rights for it - run: chmod -R 777 shuffle-database + run: chmod -R 777 shuffle-database - name: Build the stack run: docker compose up -d @@ -59,7 +60,6 @@ jobs: echo "The word 'Shuffle' was not found in the response." exit 1 fi - - name: Register a user and check the status code run: | MAX_RETRIES=30 @@ -100,31 +100,36 @@ jobs: echo "User registration failed after $MAX_RETRIES attempts." exit 1 + - name: Run Selenium testing for frontend + run: | + cd $GITHUB_WORKSPACE/frontend + # write some log to see the current directory + chmod +x frontend-testing.sh + ./frontend-testing.sh + - name: Get the API key and run a health check + id: health_check run: | RESPONSE=$(curl -s -k -u admin:StrongShufflePassword321! 'https://localhost:9200/users/_search') + echo "Raw Response: $RESPONSE" + API_KEY=$(echo "$RESPONSE" | jq -r '.hits.hits[0]._source.apikey') - if [ -n "$API_KEY" ]; then + if [ -n "$API_KEY" ] && [ "$API_KEY" != "null" ]; then echo "Admin API key: $API_KEY" + echo "API_KEY=$API_KEY" >> $GITHUB_ENV else echo "Failed to retrieve the API key for the admin user." exit 1 fi + echo "Waiting 1 minute before sending the health API request..." + sleep 60 + + echo "Checking health API..." HEALTH_RESPONSE=$(curl -s 'http://localhost:3001/api/v1/health?force=true' \ - -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7' \ - -H 'Accept-Language: en-US,en;q=0.9' \ - -H 'Connection: keep-alive' \ - -H 'Sec-Fetch-Dest: document' \ - -H 'Sec-Fetch-Mode: navigate' \ - -H 'Sec-Fetch-Site: none' \ - -H "Authorization: Bearer $API_KEY" \ - -H 'Sec-Fetch-User: ?1' \ - -H 'Upgrade-Insecure-Requests: 1' \ - -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36' \ - -H 'sec-ch-ua: "Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"' \ - -H 'sec-ch-ua-mobile: ?0' \ - -H 'sec-ch-ua-platform: "macOS"') + -H "Authorization: Bearer $API_KEY") + + echo "Health API Response: $HEALTH_RESPONSE" WORKFLOWS_CREATE=$(echo "$HEALTH_RESPONSE" | jq -r '.workflows.create') WORKFLOWS_RUN=$(echo "$HEALTH_RESPONSE" | jq -r '.workflows.run') @@ -132,9 +137,35 @@ jobs: WORKFLOWS_RUN_STATUS=$(echo "$HEALTH_RESPONSE" | jq -r '.workflows.run_status') WORKFLOWS_DELETE=$(echo "$HEALTH_RESPONSE" | jq -r '.workflows.delete') + echo "WORKFLOWS_CREATE: $WORKFLOWS_CREATE" + echo "WORKFLOWS_RUN: $WORKFLOWS_RUN" + echo "WORKFLOWS_RUN_FINISHED: $WORKFLOWS_RUN_FINISHED" + echo "WORKFLOWS_RUN_STATUS: $WORKFLOWS_RUN_STATUS" + echo "WORKFLOWS_DELETE: $WORKFLOWS_DELETE" + if [ "$WORKFLOWS_CREATE" = "true" ] && [ "$WORKFLOWS_RUN" = "true" ] && [ "$WORKFLOWS_RUN_FINISHED" = "true" ] && [ "$WORKFLOWS_RUN_STATUS" = "FINISHED" ] && [ "$WORKFLOWS_DELETE" = "true" ]; then echo "Health endpoint check was successful." + exit 0 else - echo "Health endpoint check failed. Response did not meet expected criteria." + echo "Health check failed." exit 1 fi + + notify: + needs: build + if: failure() + runs-on: ubuntu-latest + steps: + - name: Send Twilio Alert + run: | + WORKFLOW_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + MESSAGE="🚨 Shuffle Workflow Failed! + Repository: ${{ github.repository }} + Branch: ${{ github.ref_name }} + Workflow URL: $WORKFLOW_URL" + + curl -s -X POST https://api.twilio.com/2010-04-01/Accounts/${{ secrets.TWILIO_ACCOUNT_SID }}/Messages.json \ + --data-urlencode "To=${{ secrets.TWILIO_TO_NUMBER }}" \ + --data-urlencode "From=${{ secrets.TWILIO_FROM_NUMBER }}" \ + --data-urlencode "Body=$MESSAGE" \ + -u "${{ secrets.TWILIO_ACCOUNT_SID }}:${{ secrets.TWILIO_AUTH_TOKEN }}" > /dev/null diff --git a/frontend/frontend-testing.sh b/frontend/frontend-testing.sh new file mode 100644 index 00000000..c25c3618 --- /dev/null +++ b/frontend/frontend-testing.sh @@ -0,0 +1,74 @@ + +SERVER_URL="http://localhost:3000" + +declare -a Routes=($(grep -oP '(?<=path=")[^"]*' src/App.jsx | grep -E '^[a-zA-Z0-9/_:-]+$' | grep -vE '^/$')) + +for i in "${!Routes[@]}"; do + Routes[$i]="${Routes[$i]#/}" +done + +# # Add all tab routes and unique routes here +Routes+=( +'workflows?tab=org_workflows' +'workflows?tab=my_workflows' +'workflows?tab=all_workflows' +'apps/gmail' +'apis/gmail' +'apps?tab=my_apps' +'apps?tab=all_apps' +'search?tab=org_apps' +'search?tab=my_apps' +'search?tab=workflows' +'search?tab=docs' +'search?tab=creators' +'search?tab=discord' +"admin?tab=organization" +"admin?tab=users" +"admin?tab=app_auth" +"admin?tab=datastore" +"admin?tab=files" +"admin?tab=triggers" +"admin?tab=locations" +"admin?tab=tenants" +"admin?admin_tab=org_config" +"admin?admin_tab=sso" +"admin?admin_tab=notifications" +"admin?admin_tab=billingstats" +"admin?admin_tab=branding(beta)" +) + +# Stop frontend container so it test unpushed changes +docker stop shuffle-frontend + +# # Install all frontend dependencies +yarn add selenium-webdriver --dev && yarn install +echo "Starting frontend..." + +BROWSER=none yarn start & +SERVER_PID=$! +echo "Frontend started with PID: $SERVER_PID" + +echo "Waiting for 1 minute to ensure the server is fully up..." +sleep 60 + +echo "Server is up! Starting Selenium tests..." + +echo "Starting frontend tests..." +node selenium-test.js "$SERVER_URL" "${Routes[@]}" + +TEST_EXIT_CODE=$? + +if [[ $TEST_EXIT_CODE -ne 0 ]]; then +echo "Selenium tests failed. Exiting..." +kill $SERVER_PID +exit 1 +fi + +kill $SERVER_PID +echo "Testing complete. See above logs for errors if any." + +# Starting shuffle-frontend container +echo "Starting shuffle-frontend container..." +docker start shuffle-frontend +echo "shuffle-frontend started successfully." +exit 0 diff --git a/frontend/package.json b/frontend/package.json index 7426ee23..2255f955 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -128,6 +128,7 @@ "babel-preset-es2015": "^6.24.1", "postcss": "^8.4.38", "promise-window": "^1.2.1", + "selenium-webdriver": "^4.29.0", "webpack-cli": "^5.1.4" } } diff --git a/frontend/selenium-test.js b/frontend/selenium-test.js new file mode 100644 index 00000000..42b6b801 --- /dev/null +++ b/frontend/selenium-test.js @@ -0,0 +1,253 @@ +const { Builder, By, until } = require('selenium-webdriver'); +const chrome = require('selenium-webdriver/chrome'); +const fs = require('fs'); +const path = require('path'); + +console.log('Starting Selenium script for testing pages...'); + +(async () => { + const userDataDir = path.join(__dirname, 'chrome-user-data', `${Date.now()}-${Math.random().toString(36).substring(2, 8)}`); + fs.mkdirSync(userDataDir, { recursive: true }); + + let options = new chrome.Options(); + options.addArguments('--no-sandbox'); + options.addArguments('--headless'); + + let driver = await new Builder() + .forBrowser('chrome') + .setChromeOptions(options) + .build(); + + const SuccessfullyLoadedPath = []; + const FailedToLoadPath = []; + + try { + await driver.manage().window().setRect({ width: 1600, height: 1200 }); + await driver.manage().window().maximize() + await driver.manage().setTimeouts({ implicit: 15000 }); + + const frontendURL = process.argv[2]; + const routes = process.argv.slice(3); + + console.log('Frontend URL:', frontendURL); + const isCloud = frontendURL === 'http://localhost:3002' || frontendURL === 'https://sandbox.shuffler.io' || frontendURL === 'https://shuffler.io'; + + if (isCloud) { + // Login Credentials + const LOGIN_URL = `${frontendURL}/login`; + // put your login credentials here + const USERNAME = ''; + const PASSWORD = ''; + + console.log('Logging in...'); + await driver.get(LOGIN_URL); + + try { + await driver.wait(until.elementLocated(By.css('#emailfield')), 10000); + await driver.findElement(By.css('#emailfield')).sendKeys(USERNAME); + + await driver.wait(until.elementLocated(By.css('#outlined-password-input')), 10000); + await driver.findElement(By.css('#outlined-password-input')).sendKeys(PASSWORD); + + await driver.wait(until.elementLocated(By.css('#loginButton')), 10000); + await driver.findElement(By.css('#loginButton')).click(); + + // Ensure login success by checking URL change + await driver.wait(async () => { + const url = await driver.getCurrentUrl(); + return url.includes('welcome'); + }, 20000); + + console.log('Successfully logged in!'); + } catch (error) { + console.error('Login failed:', error.message); + FailedToLoadPath.push(LOGIN_URL); + await driver.quit(); + process.exit(1); + } + }else { + // Steps for onprem testing + // 1. Login + // 2. Create new workflow + + // Write your own login credentials here if you are testing onprem locally + const USERNAME = 'demo@demo.io'; + const PASSWORD = 'supercoolpassword'; + const WORKFLOW_URL = `${frontendURL}/workflows`; + + try { + // Login + console.log('Logging in...'); + await driver.get(`${frontendURL}/login`); + + await driver.wait(until.elementLocated(By.css('#emailfield')), 10000); + await driver.findElement(By.css('#emailfield')).sendKeys(USERNAME); + + await driver.wait(until.elementLocated(By.css('#outlined-password-input')), 10000); + await driver.findElement(By.css('#outlined-password-input')).sendKeys(PASSWORD); + + const loginButton = await driver.findElement(By.css('#loginButton')); + await driver.executeScript("arguments[0].scrollIntoView(true);", loginButton); + await loginButton.click(); + + + + // Ensure signup success by checking URL change + await driver.wait(async () => { + const url = await driver.getCurrentUrl(); + return url.includes('welcome') || url.includes('workflows'); + }, 20000); + + + const isParentPresent = await driver.wait(async () => { + return await driver.executeScript( + "return document.querySelector('.parent-component') !== null;" + ); + }, 5000).catch(() => false); + + if (!isParentPresent) { + const logs = await driver.manage().logs().get('browser'); + const severeErrors = logs.filter(log => log.level.name === 'SEVERE'); + if (severeErrors.length > 0) { + const crashCausingErrors = severeErrors.filter(err => { + return !err.message.includes('Warning:') && + !err.message.includes('MUI:') + }); + + console.error(`Page (${frontendURL}/login) did not load correctly:`, crashCausingErrors); + FailedToLoadPath.push(`${frontendURL}/login`); + } + } + + console.log('Successfully logged in!'); + + } catch (error) { + console.error('Failed to login:', error.message); + FailedToLoadPath.push(WORKFLOW_URL); + await driver.quit(); + process.exit(1); + } + + try { + // Create new workflow + console.log('Creating new workflow...'); + await driver.get(WORKFLOW_URL); + + await driver.sleep(1000); + + await driver.wait(until.elementLocated(By.css('#create_workflow_button')), 10000); + const create_workflow_button = await driver.findElement(By.css('#create_workflow_button')); + + await driver.sleep(1000); + await create_workflow_button.click(); + + await driver.sleep(1500); + + await driver.wait(until.elementLocated(By.css('#Enter-Workflow-Name')), 10000); + const enter_workflow_name_field = await driver.findElement(By.css('#Enter-Workflow-Name')); + + await driver.sleep(1000); + await enter_workflow_name_field.sendKeys("Test Workflow"); + + // Wait before saving + await driver.sleep(1500); + + await driver.wait(until.elementLocated(By.css('#save_workflow_button')), 10000); + const save_workflow_button = await driver.findElement(By.css('#save_workflow_button')); + + await driver.sleep(1000); // Delay before clicking + await save_workflow_button.click(); + + // Wait for the parent component to appear + await driver.sleep(2000); + + const isParentPresent = await driver.wait(async () => { + return await driver.executeScript( + "return document.querySelector('.parent-component') !== null;" + ); + }, 5000).catch(() => false); + + if (!isParentPresent) { + const logs = await driver.manage().logs().get('browser'); + const severeErrors = logs.filter(log => log.level.name === 'SEVERE'); + if (severeErrors.length > 0) { + const crashCausingErrors = severeErrors.filter(err => { + return !err.message.includes('Warning:') && + !err.message.includes('MUI:'); + }); + + console.error(`Page (${WORKFLOW_URL}) did not load correctly:`, crashCausingErrors); + FailedToLoadPath.push(`${WORKFLOW_URL}`); + } + } + + } catch (error) { + console.error('Failed to create new workflow:', error.message); + FailedToLoadPath.push(WORKFLOW_URL); + await driver.quit(); + process.exit(1); + } + } + + // Get routes from command line arguments + if (routes.length === 0) { + console.error('No routes found to test.'); + await driver.quit(); + process.exit(1); + } + console.log(`Found ${routes.length} routes to test.`); + + for (const route of routes) { + const url = `${frontendURL}/${route}`; + console.log(`Testing route: ${url}`); + + try { + await driver.get(url); + await driver.sleep(3000); + + // Wait for document readiness + await driver.wait(async () => { + return await driver.executeScript('return document.readyState') === 'complete'; + }, 10000); + + const isParentPresent = await driver.wait(async () => { + return await driver.executeScript( + "return document.querySelector('.parent-component') !== null;" + ); + }, 5000).catch(() => false); + + if (!isParentPresent) { + const logs = await driver.manage().logs().get('browser'); + const severeErrors = logs.filter(log => log.level.name === 'SEVERE'); + if (severeErrors.length > 0) { + const crashCausingErrors = severeErrors.filter(err => { + return !err.message.includes('Warning:') && + !err.message.includes('MUI:') + }); + + console.error(`Page (${url}) did not load correctly:`, crashCausingErrors); + FailedToLoadPath.push(url); + } + continue; + } + + console.log(`Successfully loaded: ${url}`); + SuccessfullyLoadedPath.push(url); + } catch (error) { + console.error(`Failed to load ${url}:`, error.message); + } + } + } finally { + console.log("Total pages tested: ", SuccessfullyLoadedPath.length + FailedToLoadPath.length); + console.log("Successfully loaded pages: ", SuccessfullyLoadedPath.length); + if (FailedToLoadPath.length > 0) { + console.log("Failed to load pages: ", FailedToLoadPath.length); + console.log("Failed to load pages paths: ", FailedToLoadPath); + process.exit(1); + }else { + console.log("No pages failed to load. Congrats!"); + } + await driver.quit(); + fs.rmSync(userDataDir, { recursive: true, force: true }); + } +})(); \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 392db7f6..7fa7c847 100755 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -189,6 +189,7 @@ const App = (message, props) => { color: "rgba(255, 255, 255, 0.65)", minHeight: "100vh", }} + className='parent-component' > { element={ {