How to Automate Avatar Upload Testing (Step-by-Step)
How to Automate Avatar Upload Testing (Step-by-Step) is a practical guide for developers and QA engineers who need reliable, repeatable verification of avatar upload flows. The first step is to recogn
How to Automate Avatar Upload Testing (Step-by-Step) is a practical guide for developers and QA engineers who need reliable, repeatable verification of avatar upload flows. The first step is to recognize that manual checks are slow, error‑prone, and scale poorly when the feature touches multiple entry points (profile page, settings, onboarding) and must work across browsers or devices. Automating the flow gives you fast feedback on regressions, lets you run the same validation on every pull request, and frees testers to focus on exploratory work that only humans can do.
Below is a complete, step‑by‑step walkthrough that covers when automation pays off, how to pick a framework, how to build locators that survive UI changes, how to tame waits and flakiness, how to manage test data, how to run the tests in CI, and how to report results. Each section includes concrete examples, code snippets, and tables you can copy into your own repository.
1. When Automation Pays Off for Avatar Upload
Avatar upload is a deceptively simple interaction: the user clicks an avatar placeholder, selects an image file from the device, confirms the choice, and sees the new picture rendered. In practice, the flow hides several failure modes that are costly to catch manually:
- File‑type validation – the backend may reject certain MIME types or dimensions, returning a toast or inline error.
- Size limits – oversized files trigger client‑side warnings or server‑side 413 responses.
- Permission handling – on mobile, the app must request gallery or camera access; on web, the file picker may be blocked by popup blockers.
- UI state – spinners, skeleton screens, or toast messages appear while the upload proceeds; missing or mis‑timed indicators confuse users.
- Accessibility – the upload button must be reachable via keyboard, have an accessible name, and announce success/failure to screen readers.
- Security – malicious filenames or embedded scripts should never reach the storage layer.
When any of these conditions change (e.g., a new image‑processing service is added, a design refresh swaps the avatar placeholder, or a privacy regulation adds a consent step), manual regression testing requires a tester to repeat the same steps on every affected platform. Automation turns that repetitive work into a fast, deterministic check that runs on every commit.
A quick rule of thumb: if you need to verify the avatar upload on more than two environments (e.g., Chrome, Firefox, Safari, Android, iOS) or you run the test more than once per week, automation will save time after the initial investment. The table below shows typical effort estimates for a small team.
| Effort factor | Manual (per run) | Automated (per run) | Break‑even point |
|---|---|---|---|
| Test execution time | 4‑6 min (setup, click, verify) | 20‑30 s (including CI overhead) | After ~8‑10 runs |
| Maintenance overhead | Low (just follow the script) | Moderate (selector updates, flakiness fixes) | After ~2‑3 weeks of stable UI |
| Coverage | Limited to tester’s availability | Unlimited (parallel runs, nightly) | Immediate once stable |
| Flakiness risk | Human error, intermittent observation | Can be mitigated with waits/retry | Requires investment in stable locators |
If your team already runs nightly smoke tests, adding avatar upload to that suite usually pays off within the first sprint.
2. Choosing a Test Framework
Selecting the right framework depends on three factors: the technology stack of your application, the skill set of your team, and the need for cross‑platform coverage. Below is a comparison of the most common choices for avatar upload testing.
| Framework | Language(s) | UI type | Built‑in file upload handling | Flakiness mitigation | Learning curve | CI friendliness |
|---|---|---|---|---|---|---|
| Playwright | JavaScript/TypeScript, Python, .NET, Java | Web (Chromium, Firefox, WebKit) | page.setInputFiles() works natively | Auto‑waits, network idle, trace viewer | Low‑moderate | Excellent (GitHub Actions, GitLab CI) |
| Cypress | JavaScript/TypeScript | Web (Chromium‑based) | cy.fixture() + cy.get().selectFile() | Automatic retries, time‑travel debugging | Low | Good (native dashboard) |
| Selenium/WebDriverIO | JavaScript/TypeScript, Java, C#, Python, Ruby | Web (all browsers) | sendKeys to | Explicit waits, fluent wait | Moderate | Good (requires Selenium Grid or cloud) |
| Appium | JavaScript/TypeScript, Java, Python, Ruby, C# | Mobile (Android/iOS) and hybrid web | driver.pushFile() + setValue on native input | Implicit/explicit waits, image‑based fallback | Moderate‑high | Good (requires emulator/simulator farm) |
| Espresso/XCUITest | Java/Kotlin (Android), Swift/Obj‑C (iOS) | Native mobile only | onView(withId(...)).perform(replaceText(...)) for Espresso; XCUIElement for XCUITest | Idling resources, synchronization APIs | High (platform‑specific) | Excellent (integrates with Gradle/Xcode build) |
How to decide
- If your avatar upload lives in a single‑page web app and you already write JavaScript/TypeScript, Playwright offers the best combination of auto‑waits, multi‑browser support, and straightforward file‑upload API.
- For teams heavily invested in Cypress for other web tests, staying within that framework avoids context switching; just note that Cypress runs only in Chromium‑based browsers (though you can test Firefox via the experimental
cypress-firefoxplugin). - If you need to test native mobile apps (Android/iOS) where the avatar picker invokes the system gallery, Appium is the most versatile cross‑platform option. For pure native UI tests, Espresso (Android) or XCUITest (iOS) give the fastest execution but require platform‑specific code.
- When you already have a Selenium Grid in place and your team is comfortable with Java, WebDriverIO is a safe fallback.
Whichever framework you choose, make sure it supports file upload via setting the value of a hidden element (or the platform‑equivalent) because interacting with the native OS file dialog directly is notoriously flaky and often blocked in CI environments.
3. Building a Stable Locator Strategy
Flaky tests often trace back to brittle selectors that break when a designer tweaks a class name or adds a wrapper div. For avatar upload, the elements you need to interact with are:
- The avatar placeholder or button that opens the file picker.
- The hidden
(or native file‑picker trigger) that receives the file path. - An indicator that the upload has started (spinner, progress bar).
- A success indicator (toast, updated avatar image, API response).
- An error indicator (toast, inline message, disabled button).
3.1 Prefer data‑testid or ARIA attributes
Ask developers to add a stable attribute such as data-testid="avatar-upload-button" or aria-label="Upload avatar" to the clickable element. In the test, locate it by that attribute rather than by a CSS class that may change.
// Playwright example
const uploadBtn = page.locator('[data-testid="avatar-upload-button"]');
await uploadBtn.click();
If you cannot change the source, fall back to a combination of role and accessible name:
// WebDriverIO example
const uploadBtn = $('button[aria-label="Upload avatar"]');
3.2 Avoid positional or index‑based selectors
Selectors like :nth-child(2) or //div[3]/button break when a new banner is inserted above the avatar section. Instead, locate by unique text (if it’s static and translatable‑safe) or by a nearby static landmark.
// Using a nearby heading as an anchor
const heading = page.getByText('Profile settings', { exact: true });
const uploadBtn = heading.locator('..').getByTestId('avatar-upload-button');
3.3 Handling dynamic IDs
Some frameworks generate IDs like avatar-upload-1a2b3c. If you must use them, extract the stable part with a regular expression or use a CSS attribute selector that matches a prefix:
[id^="avatar-upload-"] /* matches any ID that starts with the prefix */
In Playwright:
const uploadBtn = page.locator('input[id^="avatar-upload-"][type="file"]');
3.4 Mobile‑specific considerations
On Android, the file picker is a native dialog; you cannot interact with it via UIAutomator unless you set the file path directly on the underlying EditText or ImageView that backs the picker. The usual approach is:
- Click the avatar image (which opens the system picker).
- Use
adb shell am broadcast -a android.intent.action.PICK ...or rely on Appium’ssetValueon the native input after switching to theWEBVIEWcontext (if the app uses a web view for the picker). - For pure native apps, push the file to the device with
adb pushand then set the path on the exposed input field.
On iOS, the equivalent is using XCUITest to tap the avatar, then using addAttachment (if the app uses UIImagePickerController) or setting the value of the hidden file input in a web view.
3.5 Summary checklist for locators
- ✅ Use
data-testidoraria-labelwhere possible. - ✅ Combine role + name for resilience.
- ✅ Avoid indexes, nth‑child, and generated class names.
- ✅ Prefer attribute‑starts‑with (
[id^=…]) over exact matches when IDs are dynamic. - ✅ Document the locator rationale in a comment beside each selector.
4. Taming Waits and Eliminating Flakiness
Even with perfect locators, timing issues cause the majority of flaky UI tests. Avatar upload involves asynchronous steps: opening the picker, reading the file, sending it to the backend, processing the image, and updating the UI. The key is to wait for observable state changes rather than arbitrary sleep periods.
4.1 Explicit waits over implicit waits with expected conditions
Most frameworks expose a way to wait until an element meets a condition (visible, enabled, contains text, etc.). Use those instead of page.waitForTimeout() or Thread.sleep().
Playwright (TypeScript)
// Wait for the spinner to disappear
await page.locator('[data-testid="upload-spinner"]').waitFor({ state: 'hidden' });
// Wait for the new avatar image to appear (by checking its src attribute)
await page.locator('[data-testid="avatar-image"]')
.waitFor({ state: 'attached' });
await expect(page.locator('[data-testid="avatar-image"]')).toHaveAttribute(
'src', /avatar-/
);
Appium (Java)
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));
wait.until(ExpectedConditions.invisibilityOfElementLocated(
MobileBy.accessibilityId("uploadSpinner")));
wait.until(ExpectedConditions.attributeContains(
MobileBy.id("avatarImageView"), "contentDescription", "uploaded"));
4.2 Network idle waiting
If the upload triggers an XHR/fetch request, waiting for the network to be idle ensures the request has finished before you assert UI changes.
Playwright
// Assuming the request ends with /api/avatar
await page.waitForResponse(response =>
response.url().endsWith('/api/avatar') && response.status() === 200);
WebDriverIO
browser.waitUntil(
() => browser.getLogs('browser').some(log =>
log.message.includes('/api/avatar') && log.message.includes('200')),
10000,
'waiting for avatar upload response'
);
4.3 Retry mechanisms for intermittent issues
Some flakiness originates from occasional popup blockers, delayed animation frames, or occasional stale element references. Wrap the assertion in a retry loop with exponential backoff, or use built‑in retry utilities.
Cypress
// Cypress automatically retries commands until they pass or timeout
cy.get('[data-testid="avatar-image"]')
.should('have.attr', 'src')
.and('match', /avatar-/);
Custom retry (JavaScript)
async function retry(fn, retries = 3, delay = 500) {
for (let i = 0; i < retries; i++) {
try { return await fn(); } catch (e) {
if (i === retries - 1) throw e;
await new Promise(r => setTimeout(r, delay * 2 ** i));
}
}
}
4.4 Disabling animations in test mode
Many CSS transitions or Lottie animations add nondeterministic timing. If you control the build, expose a feature flag (e.g., window.__TEST_MODE__ = true) that sets *{transition:none !important; animation:none !important;} or uses prefers-reduced-motion. This removes variability without affecting production code.
4.5 Capturing traces and screenshots on failure
When a test fails, automatically store a screenshot, page source, and (for Playwright) a trace file. This dramatically reduces debugging time.
Playwright config
// playwright.config.ts
export default {
testDir: './tests',
timeout: 30_000,
retries: 2,
use: {
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
};
Appium (Java) – using TestNG listeners
@Override
public void onTestFailure(ITestResult result) {
TakesScreenshot ts = (TakesScreenshot) driver;
File src = ts.getScreenshotAs(OutputType.FILE);
FileUtils.copyFile(src, new File("target/screenshots/" + result.getName() + ".png"));
}
By combining explicit waits, network idle checks, retries, and animation disabling, you can reduce flakiness to a level where the test passes reliably on every CI run.
5. Data Setup and Teardown Strategies
Avatar upload tests need a valid user session and a clean slate for each run to avoid cross‑test contamination (e.g., leftover avatar from a previous test causing false positives). The approach differs slightly between web and mobile, but the principles are the same.
5.1 Creating a disposable test user
- Use a dedicated test tenant or a feature flag that bypasses email verification.
- Generate a random username/email via a fixture factory (e.g.,
faker.js,Factory Boy, orJava Faker). - Call the registration API directly in a
beforeEachhook, store the auth token, and set it in the browser’slocalStorageor send it as an Authorization header.
Playwright example (setup hook)
import { test as base } from '@playwright/test';
import { faker } from '@faker-js/faker';
import axios from 'axios';
type TestFixtures = {
userEmail: string;
authToken: string;
};
export const test = base.extend<TestFixtures>({
userEmail: [async ({}, use) => {
const email = faker.internet.email();
await use(email);
}, { scope: 'test' }],
authToken: [async ({ userEmail }, use) => {
const res = await axios.post('https://api.example.com/register', {
email: userEmail,
password: 'TestPass!123',
});
await use(res.data.token);
}, { scope: 'test' }],
});
In the test, set the token:
await page.context().addCookies([
{ name: 'token', value: authToken, domain: '.example.com', path: '/' }
]);
await page.goto('/profile');
5.2 Preparing the avatar file
Avoid relying on a static image that lives in the repo; instead generate a small binary blob on the fly. This guarantees the file is unique (helps with caching) and lets you test different formats and sizes.
Node (Playwright)
const { createCanvas } = require('canvas');
const fs = require('fs');
function generatePNG(width = 200, height = 200) {
const canvas = createCanvas(width, height);
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#' + Math.floor(Math.random()*0xffffff).toString(16);
ctx.fillRect(0, 0, width, height);
return canvas.toBuffer('image/png');
}
// In test
const avatarBuffer = generatePNG();
await page.setInputFiles('[data-testid="avatar-file-input"]',
[{ name: 'avatar.png', mimeType: 'image/png', buffer: avatarBuffer }]);
Appium (Java)
Bitmap bitmap = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(bitmap);
c.drawColor(Color.parseColor("#" + Integer.toHexString(new Random().nextInt())));
ByteArrayOutputStream stream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
byte[] pngBytes = stream.toByteArray();
// Push to device
String remotePath = "/data/local/tmp/avatar.png";
driver.pushFile(remotePath, Base64.encodeToString(pngBytes, Base64.NO_WRAP));
// Set the file input (assuming a web view)
WebElement fileInput = driver.findElement(By.css("input[type='file']"));
fileInput.sendKeys(remotePath);
5.3 Cleaning up after each test
- Delete the uploaded avatar via a DELETE API call (
/api/avatar/:id) using the same auth token. - If the backend does not expose a delete endpoint, rely on a test‑specific bucket or folder that is purged nightly, and add a unique identifier (e.g., UUID) to the filename so you can locate and remove it later.
- Clear cookies or local storage to ensure the next test starts with a fresh session.
Playwright teardown
test.afterEach(async ({ userEmail, authToken }) => {
// Assuming we stored avatar ID in a test variable; otherwise fetch list and delete the latest
await axios.delete('https://api.example.com/avatar/me', {
headers: { Authorization: `Bearer ${authToken}` }
});
});
5.4 Using fixtures for reusable data
Define a fixture that returns an object containing the user credentials, auth token, and a temporary file path. This keeps the test body focused on actions rather than setup boilerplate.
By isolating data creation and cleanup, you guarantee that each avatar upload test runs against a known‑good state, making failures easier to attribute to the feature under test rather than to leftover data.
6. Writing the Test: Step‑by‑Step Code Samples
Below are complete, ready‑to‑run examples for both web (Playwright) and mobile (Appium) that illustrate the full flow: login, navigate to avatar upload, select a generated image, wait for upload, verify success, and clean up.
6.1 Web – Playwright (TypeScript)
Create a file tests/avatar-upload.spec.ts.
import { test, expect } from '@playwright/test';
import { faker } from '@faker-js/faker';
import axios from 'axios';
// Helper to generate a small PNG blob
function generatePNG(): Buffer {
const { createCanvas } = require('canvas');
const canvas = createCanvas(150, 150);
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#' + Math.floor(Math.random()*0xffffff).toString(16);
ctx.fillRect(0, 0, 150, 150);
return canvas.toBuffer('image/png');
}
test.describe('Avatar upload flow', () => {
let authToken: string;
let userEmail: string;
test.beforeEach(async ({}) => {
// 1. Create a disposable user
userEmail = faker.internet.email();
const regRes = await axios.post('https://api.example.com/register', {
email: userEmail,
password: 'TestPass!123',
});
authToken = regRes.data.token;
});
test.afterEach(async ({}) => {
// 2. Clean up avatar
await axios.delete('https://api.example.com/avatar/me', {
headers: { Authorization: `Bearer ${authToken}` }
});
});
test('user can upload a new avatar and see it updated', async ({ page }) => {
// 3. Log in via API and set cookie
await page.context().addCookies([
{ name: 'token', value: authToken, domain: '.example.com', path: '/' }
]);
// 4. Navigate to profile page
await page.goto('/profile');
await expect(page.locator('h1')).toHaveText(/Profile/i);
// 5. Click avatar placeholder to open file picker
const uploadBtn = page.locator('[data-testid="avatar-upload-button"]');
await uploadBtn.click();
// 6. Set generated PNG on the hidden file input
const fileInput = page.locator('input[type="file"]');
const avatarBuffer = generatePNG();
await fileInput.setInputFiles([
{ name: 'avatar.png', mimeType: 'image/png', buffer: avatarBuffer }
]);
// 7. Wait for spinner to disappear (upload in progress)
const spinner = page.locator('[data-testid="upload-spinner"]');
await spinner.waitFor({ state: 'visible' });
await spinner.waitFor({ state: 'hidden' });
// 8. Verify success toast appears
const toast = page.locator('[data-testid="toast-success"]');
await expect(toast).toContainText('Avatar updated', { timeout: 5000 });
// 9. Verify the avatar image src changed
const avatarImg = page.locator('[data-testid="avatar-image"]');
await expect(avatarImg).toHaveAttribute('src', /avatar-/, { timeout: 5000 });
// Optional: verify the image actually changed by checking a hash
// (requires downloading the image and comparing bytes; omitted for brevity)
});
});
Key points illustrated
- Disposable user creation via API.
- Authentication via cookie (avoids UI login flakiness).
- Use of
data-testidfor stable locating. - Generation of a unique PNG blob on the fly.
- Explicit wait for spinner visibility then invisibility.
- Assertion on toast and image attribute change.
6.2 Mobile – Appium (Java) with Android emulator
Assume the app is a hybrid where the avatar picker opens a native file chooser; we will push the image to the device and set the path on the native input.
Create src/test/java/com/example/avatar/AvatarUploadTest.java.
package com.example.avatar;
import io.appium.java_client.AppiumDriver;
import io.appium.java_client.MobileElement;
import io.appium.java_client.android.AndroidDriver;
import org.junit.*;
import org.openqa.selenium.By;
import org.openqa.selenium.Dimension;
import org.openqa.selenium.remote.DesiredCapabilities;
import java.net.URL;
import java.util.Base64;
import java.util.concurrent.TimeUnit;
public class AvatarUploadTest {
private AppiumDriver<MobileElement> driver;
private String authToken; // obtained via a pre‑login API call (omitted for brevity)
@Before
public void setUp() throws Exception {
DesiredCapabilities caps = new DesiredCapabilities();
caps.setCapability("platformName", "Android");
caps.setCapability("deviceName", "Pixel_4_API_33");
caps.setCapability("automationName", "UiAutomator2");
caps.setCapability("appPackage", "com.example.app");
caps.setCapability("appActivity", ".MainActivity");
caps.setCapability("noReset", false);
driver = new AndroidDriver<>(new URL("http://localhost:4723/wd/hub"), caps);
driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
// Assume we have logged in via API and set the token in shared preferences
authToken = obtainAuthTokenViaApi(); // helper method not shown
}
@After
public void tearDown() {
if (driver != null) {
// Delete avatar via backend API
deleteAvatarViaApi(authToken);
driver.quit();
}
}
@Test
public void testAvatarUpload() throws Exception {
// 1. Launch app and navigate to profile screen
MobileElement profileTab = driver.findElement(By.id("nav_profile"));
profileTab.click();
MobileElement avatarImg = driver.findElement(By.id("avatar_image"));
Assert.assertTrue(avatarImg.isDisplayed());
// 2. Tap avatar to open picker
avatarImg.click();
// 3. Prepare a small PNG image (150x150) and push to device
Bitmap bitmap = Bitmap.createBitmap(150, 150, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.drawColor(android.graphics.Color.parseColor("#" + Integer.toHexString(new java.util.Random().nextInt())));
ByteArrayOutputStream stream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
byte[] pngBytes = stream.toByteArray();
String remotePath = "/data/local/tmp/avatar_" + System.currentTimeMillis() + ".png";
driver.pushFile(remotePath, Base64.encodeToString(pngBytes, Base64.NO_WRAP));
// 4. Set the path on the native file input (assuming the picker uses an EditText with id "file_path")
MobileElement fileInput = driver.findElement(By.id("file_path"));
fileInput.clear();
fileInput.sendKeys(remotePath);
// 5. Confirm selection (often a button with text "OK")
MobileElement okBtn = driver.findElement(By.androidUIAutomator(
"new UiSelector().text(\"OK\")"));
okBtn.click();
// 6. Wait for progress spinner to disappear
MobileElement spinner = driver.findElement(By.id("upload_spinner"));
new WebDriverWait(driver, 20)
.until(ExpectedConditions.invisibilityOfElementLocated(By.id("upload_spinner")));
// 7. Verify success toast
MobileElement toast = driver.findElement(By.id("toast_message"));
Assert.assertTrue(toast.getText().contains("Avatar updated"));
// 8. Verify avatar image updated (check content description changes)
MobileElement newAvatar = driver.findElement(By.id("avatar_image"));
Assert.assertEquals(newAvatar.getAttribute("contentDescription"), "avatar_updated");
}
// Placeholder helpers – implement using your backend API
private String obtainAuthTokenViaApi() { return "dummy-token"; }
private void deleteAvatarViaApi(String token) { /* HTTP DELETE */ }
}
Explanation of the mobile flow
- The test starts with a clean app state (
noReset:false). - It navigates to the profile tab, taps the avatar image to trigger the native picker.
- A small PNG bitmap is generated in‑memory, pushed to
/data/local/tmpon the emulator/device, and the path is sent to the file‑input field. - After confirming the selection, we wait for the spinner to disappear, then assert a toast and a change in the avatar’s content description.
- The
@Aftermethod removes the uploaded avatar via a backend DELETE call.
Both examples demonstrate the core concepts: disposable test data, stable locators, explicit waits, and teardown. You can adapt them to your stack (Cypress, Selenium, Espresso, etc.) by swapping the locating and waiting APIs while keeping the same logical steps.
7. Running the Tests in CI
Automated tests only deliver value when they run on every change and give fast feedback. Below is a concise guide for integrating the avatar upload tests into two common CI systems: GitHub Actions and Jenkins. Adjust the steps for GitLab CI, Azure Pipelines, or Bitbucket Pipelines as needed.
7.1 GitHub Actions (Playwright)
Create .github/workflows/avatar-upload.yml.
name: Avatar Upload CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
test:
runs-on: ubuntu-latest
services:
# If your backend runs locally, start it as a service container
backend:
image: your-backend-image:latest
ports: [ 8080:8080 ]
env:
DB_URL: postgres://postgres:password@localhost:5432/testdb
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Start backend (if not using service)
run: |
npm run start:backend &
npx wait-on http://localhost:8080/health
- name: Run Playwright tests
env:
TEST_BASE_URL: http://localhost:3000
run: npx playwright test tests/avatar-upload.spec.ts --reporter=html
- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@v3
with:
name: playwright-report
path: playwright-report/
- name: Upload test results (JUnit)
if: always()
uses: actions/upload-artifact@v3
with:
name: junit-results
path: junit/
What this does
- Spins up a backend service (or you can point to a staging environment).
- Installs Node dependencies.
- Starts the frontend if needed, waits for a health endpoint.
- Executes the Playwright test, generating an HTML report and a JUnit XML file (you can add `--output=
Test Your App Autonomously
Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.
Try SUSA Free