QA Test Bots
by @nicemaths123
Automate end-to-end QA tests simulating user flows, checking UI elements, broken links, responsiveness, and performance across devices with Apify integration.
clawhub install qa-test-botsπ About This Skill
Automated QA Testing Bots Skill
Overview
This skill enables Claude to build and run automated QA testing bots that simulate real user journeys across websites and web applications β catching bugs, broken flows, and regressions before your users do.
Powered by Apify with Puppeteer and Playwright under the hood.
> π Sign up for Apify here: https://www.apify.com/?fpr=dx06p
What This Skill Does
Step 1 β Get Your Apify API Token
1. Go to https://www.apify.com/?fpr=dx06p and create a free account
2. Navigate to Settings β Integrations
- Direct link: https://console.apify.com/account/integrations
3. Copy your Personal API Token: apify_api_xxxxxxxxxxxxxxxx
4. Set it as an environment variable:
export APIFY_TOKEN=apify_api_xxxxxxxxxxxxxxxx
Step 2 β Install Dependencies
npm install apify-client
Actors for QA Testing
| Actor ID | Best For |
|---|---|
| apify/puppeteer-scraper | Full browser automation, form testing, click flows |
| apify/playwright-scraper | Cross-browser testing (Chrome, Firefox, WebKit) |
| apify/broken-links-checker | Detect all 404s and broken links site-wide |
| apify/website-content-crawler | Crawl all pages and validate structure |
Examples
Test a Full User Registration Flow
import ApifyClient from 'apify-client';const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor("apify/puppeteer-scraper").call({
startUrls: [{ url: "https://your-app.com/signup" }],
pageFunction: async function pageFunction(context) {
const { page } = context;
const results = { steps: [], passed: true, errors: [] };
try {
// Step 1 β Page loads
await page.waitForSelector('#signup-form', { timeout: 5000 });
results.steps.push({ step: "Page loaded", status: "PASS" });
// Step 2 β Fill registration form
await page.type('#firstName', 'Test');
await page.type('#lastName', 'User');
await page.type('#email', testuser+${Date.now()}@example.com);
await page.type('#password', 'SecurePass123!');
results.steps.push({ step: "Form filled", status: "PASS" });
// Step 3 β Submit
await Promise.all([
page.waitForNavigation({ timeout: 8000 }),
page.click('button[type="submit"]')
]);
results.steps.push({ step: "Form submitted", status: "PASS" });
// Step 4 β Assert success redirect
const currentUrl = page.url();
if (!currentUrl.includes('/dashboard')) {
throw new Error(Expected /dashboard, got: ${currentUrl});
}
results.steps.push({ step: "Redirected to dashboard", status: "PASS" });
// Step 5 β Screenshot proof
await page.screenshot({ path: 'signup-success.png', fullPage: true });
} catch (err) {
results.passed = false;
results.errors.push(err.message);
await page.screenshot({ path: 'signup-error.png', fullPage: true });
}
return results;
}
});
const { items } = await run.dataset().getData();
const report = items[0];
console.log(report.passed ? "β
All steps passed" : "β Test failed");
report.steps.forEach(s => console.log( [${s.status}] ${s.step}));
if (report.errors.length) console.log("Errors:", report.errors);
Test a Complete E-Commerce Checkout Flow
const run = await client.actor("apify/puppeteer-scraper").call({
startUrls: [{ url: "https://your-shop.com/products/test-item" }],
pageFunction: async function pageFunction(context) {
const { page } = context;
const journey = []; // 1 β Product page
await page.waitForSelector('.add-to-cart');
journey.push({ step: "Product page loaded", status: "PASS" });
// 2 β Add to cart
await page.click('.add-to-cart');
await page.waitForSelector('.cart-count', { timeout: 3000 });
const cartCount = await page.$eval('.cart-count', el => el.innerText);
journey.push({
step: "Item added to cart",
status: cartCount > 0 ? "PASS" : "FAIL",
value: cartCount
});
// 3 β Go to cart
await page.click('.cart-icon');
await page.waitForSelector('.cart-summary');
journey.push({ step: "Cart page loaded", status: "PASS" });
// 4 β Proceed to checkout
await page.click('.proceed-to-checkout');
await page.waitForSelector('#checkout-form');
journey.push({ step: "Checkout page loaded", status: "PASS" });
// 5 β Fill shipping info
await page.type('#shipping-name', 'QA Test User');
await page.type('#shipping-address', '123 Test Street');
await page.type('#shipping-city', 'San Francisco');
await page.type('#shipping-zip', '94105');
journey.push({ step: "Shipping info filled", status: "PASS" });
return { journey, allPassed: journey.every(s => s.status === "PASS") };
}
});
Detect All Broken Links Site-Wide
const run = await client.actor("apify/broken-links-checker").call({
startUrls: [{ url: "https://your-website.com" }],
maxCrawlingDepth: 3,
maxRequestsPerCrawl: 200
});const { items } = await run.dataset().getData();
const broken = items.filter(link => link.statusCode >= 400);
console.log(Found ${broken.length} broken links out of ${items.length} checked);
broken.forEach(link => {
console.log( [${link.statusCode}] ${link.url} β found on: ${link.referrer});
});
Responsive Design Test β Multi-Viewport
const viewports = [
{ name: "Desktop", width: 1440, height: 900 },
{ name: "Tablet", width: 768, height: 1024 },
{ name: "Mobile", width: 375, height: 812 }
];const run = await client.actor("apify/puppeteer-scraper").call({
startUrls: [{ url: "https://your-app.com" }],
pageFunction: async function pageFunction(context) {
const { page } = context;
const results = [];
const viewports = [
{ name: "Desktop", width: 1440, height: 900 },
{ name: "Tablet", width: 768, height: 1024 },
{ name: "Mobile", width: 375, height: 812 }
];
for (const vp of viewports) {
await page.setViewport({ width: vp.width, height: vp.height });
await page.reload();
const navVisible = await page.$('.navbar') !== null;
const ctaVisible = await page.$('.cta-button') !== null;
results.push({
viewport: vp.name,
resolution: ${vp.width}x${vp.height},
navbarPresent: navVisible,
ctaButtonPresent: ctaVisible,
status: navVisible && ctaVisible ? "PASS" : "FAIL"
});
}
return results;
}
});
Performance & Load Time Assertions
const run = await client.actor("apify/puppeteer-scraper").call({
startUrls: [{ url: "https://your-app.com" }],
pageFunction: async function pageFunction(context) {
const { page } = context; const startTime = Date.now();
await page.waitForSelector('main');
const loadTime = Date.now() - startTime;
const metrics = await page.metrics();
const perfEntries = await page.evaluate(() =>
JSON.stringify(window.performance.timing)
);
const timing = JSON.parse(perfEntries);
const ttfb = timing.responseStart - timing.navigationStart;
const domReady = timing.domContentLoadedEventEnd - timing.navigationStart;
return {
url: page.url(),
loadTimeMs: loadTime,
ttfbMs: ttfb,
domReadyMs: domReady,
jsHeapUsedMB: (metrics.JSHeapUsedSize / 1024 / 1024).toFixed(2),
passed: loadTime < 3000 && ttfb < 600,
warnings: [
loadTime > 3000 ? Slow load: ${loadTime}ms (threshold: 3000ms) : null,
ttfb > 600 ? High TTFB: ${ttfb}ms (threshold: 600ms) : null
].filter(Boolean)
};
}
});
QA Workflow β How Claude Uses This Skill
When asked to test a site or app, Claude will:
1. Map the user journeys to test (registration, login, checkout, search...) 2. Build a Puppeteer/Playwright test script for each journey 3. Run all tests in parallel via Apify actors 4. Collect pass/fail results, screenshots, and error messages 5. Generate a structured test report with step-by-step results 6. Flag failures with context β which step failed and why 7. Optionally schedule recurring runs after each deployment
Normalized Test Report Schema
{
"testName": "User Registration Flow",
"url": "https://your-app.com/signup",
"passed": true,
"duration": 4823,
"steps": [
{ "step": "Page loaded", "status": "PASS", "durationMs": 820 },
{ "step": "Form filled", "status": "PASS", "durationMs": 310 },
{ "step": "Form submitted", "status": "PASS", "durationMs": 2100 },
{ "step": "Redirected to dashboard","status": "PASS", "durationMs": 593 }
],
"errors": [],
"screenshotUrl": "https://api.apify.com/v2/key-value-stores/.../records/signup-success.png",
"runAt": "2025-02-25T10:00:00Z"
}
CI/CD Integration (GitHub Actions)
# .github/workflows/qa.yml
name: Automated QA Testson:
push:
branches: [main, staging]
pull_request:
branches: [main]
jobs:
qa:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run QA Tests via Apify
run: |
curl -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${{ secrets.APIFY_TOKEN }}" \
-d '{"startUrls":[{"url":"${{ vars.STAGING_URL }}"}]}' \
"https://api.apify.com/v2/acts/apify~puppeteer-scraper/runs"
Best Practices
+timestamp suffixes to avoid conflicts between runstimeout on every waitForSelector β never let a test hang indefinitelywaitForNavigation after any click that triggers a page loadError Handling
try {
const run = await client.actor("apify/puppeteer-scraper").call(input);
const dataset = await run.dataset().getData();
return dataset.items;
} catch (error) {
if (error.statusCode === 401) throw new Error("Invalid Apify token β check credentials");
if (error.statusCode === 429) throw new Error("Rate limit hit β reduce parallel test runs");
if (error.message.includes("timeout")) throw new Error("Test timed out β check if the app is reachable");
throw error;
}
Requirements
apify-clientπ‘ Examples
Test a Full User Registration Flow
import ApifyClient from 'apify-client';const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor("apify/puppeteer-scraper").call({
startUrls: [{ url: "https://your-app.com/signup" }],
pageFunction: async function pageFunction(context) {
const { page } = context;
const results = { steps: [], passed: true, errors: [] };
try {
// Step 1 β Page loads
await page.waitForSelector('#signup-form', { timeout: 5000 });
results.steps.push({ step: "Page loaded", status: "PASS" });
// Step 2 β Fill registration form
await page.type('#firstName', 'Test');
await page.type('#lastName', 'User');
await page.type('#email', testuser+${Date.now()}@example.com);
await page.type('#password', 'SecurePass123!');
results.steps.push({ step: "Form filled", status: "PASS" });
// Step 3 β Submit
await Promise.all([
page.waitForNavigation({ timeout: 8000 }),
page.click('button[type="submit"]')
]);
results.steps.push({ step: "Form submitted", status: "PASS" });
// Step 4 β Assert success redirect
const currentUrl = page.url();
if (!currentUrl.includes('/dashboard')) {
throw new Error(Expected /dashboard, got: ${currentUrl});
}
results.steps.push({ step: "Redirected to dashboard", status: "PASS" });
// Step 5 β Screenshot proof
await page.screenshot({ path: 'signup-success.png', fullPage: true });
} catch (err) {
results.passed = false;
results.errors.push(err.message);
await page.screenshot({ path: 'signup-error.png', fullPage: true });
}
return results;
}
});
const { items } = await run.dataset().getData();
const report = items[0];
console.log(report.passed ? "β
All steps passed" : "β Test failed");
report.steps.forEach(s => console.log( [${s.status}] ${s.step}));
if (report.errors.length) console.log("Errors:", report.errors);
Test a Complete E-Commerce Checkout Flow
const run = await client.actor("apify/puppeteer-scraper").call({
startUrls: [{ url: "https://your-shop.com/products/test-item" }],
pageFunction: async function pageFunction(context) {
const { page } = context;
const journey = []; // 1 β Product page
await page.waitForSelector('.add-to-cart');
journey.push({ step: "Product page loaded", status: "PASS" });
// 2 β Add to cart
await page.click('.add-to-cart');
await page.waitForSelector('.cart-count', { timeout: 3000 });
const cartCount = await page.$eval('.cart-count', el => el.innerText);
journey.push({
step: "Item added to cart",
status: cartCount > 0 ? "PASS" : "FAIL",
value: cartCount
});
// 3 β Go to cart
await page.click('.cart-icon');
await page.waitForSelector('.cart-summary');
journey.push({ step: "Cart page loaded", status: "PASS" });
// 4 β Proceed to checkout
await page.click('.proceed-to-checkout');
await page.waitForSelector('#checkout-form');
journey.push({ step: "Checkout page loaded", status: "PASS" });
// 5 β Fill shipping info
await page.type('#shipping-name', 'QA Test User');
await page.type('#shipping-address', '123 Test Street');
await page.type('#shipping-city', 'San Francisco');
await page.type('#shipping-zip', '94105');
journey.push({ step: "Shipping info filled", status: "PASS" });
return { journey, allPassed: journey.every(s => s.status === "PASS") };
}
});
Detect All Broken Links Site-Wide
const run = await client.actor("apify/broken-links-checker").call({
startUrls: [{ url: "https://your-website.com" }],
maxCrawlingDepth: 3,
maxRequestsPerCrawl: 200
});const { items } = await run.dataset().getData();
const broken = items.filter(link => link.statusCode >= 400);
console.log(Found ${broken.length} broken links out of ${items.length} checked);
broken.forEach(link => {
console.log( [${link.statusCode}] ${link.url} β found on: ${link.referrer});
});
Responsive Design Test β Multi-Viewport
const viewports = [
{ name: "Desktop", width: 1440, height: 900 },
{ name: "Tablet", width: 768, height: 1024 },
{ name: "Mobile", width: 375, height: 812 }
];const run = await client.actor("apify/puppeteer-scraper").call({
startUrls: [{ url: "https://your-app.com" }],
pageFunction: async function pageFunction(context) {
const { page } = context;
const results = [];
const viewports = [
{ name: "Desktop", width: 1440, height: 900 },
{ name: "Tablet", width: 768, height: 1024 },
{ name: "Mobile", width: 375, height: 812 }
];
for (const vp of viewports) {
await page.setViewport({ width: vp.width, height: vp.height });
await page.reload();
const navVisible = await page.$('.navbar') !== null;
const ctaVisible = await page.$('.cta-button') !== null;
results.push({
viewport: vp.name,
resolution: ${vp.width}x${vp.height},
navbarPresent: navVisible,
ctaButtonPresent: ctaVisible,
status: navVisible && ctaVisible ? "PASS" : "FAIL"
});
}
return results;
}
});
Performance & Load Time Assertions
const run = await client.actor("apify/puppeteer-scraper").call({
startUrls: [{ url: "https://your-app.com" }],
pageFunction: async function pageFunction(context) {
const { page } = context; const startTime = Date.now();
await page.waitForSelector('main');
const loadTime = Date.now() - startTime;
const metrics = await page.metrics();
const perfEntries = await page.evaluate(() =>
JSON.stringify(window.performance.timing)
);
const timing = JSON.parse(perfEntries);
const ttfb = timing.responseStart - timing.navigationStart;
const domReady = timing.domContentLoadedEventEnd - timing.navigationStart;
return {
url: page.url(),
loadTimeMs: loadTime,
ttfbMs: ttfb,
domReadyMs: domReady,
jsHeapUsedMB: (metrics.JSHeapUsedSize / 1024 / 1024).toFixed(2),
passed: loadTime < 3000 && ttfb < 600,
warnings: [
loadTime > 3000 ? Slow load: ${loadTime}ms (threshold: 3000ms) : null,
ttfb > 600 ? High TTFB: ${ttfb}ms (threshold: 600ms) : null
].filter(Boolean)
};
}
});
π Tips & Best Practices
+timestamp suffixes to avoid conflicts between runstimeout on every waitForSelector β never let a test hang indefinitelywaitForNavigation after any click that triggers a page load