mirror of
https://github.com/Theodor-Springmann-Stiftung/musenalm.git
synced 2026-02-04 02:25:30 +00:00
BUGFIX: stresstest u select-vals
This commit is contained in:
187
test/clicker/clicker.mjs
Normal file
187
test/clicker/clicker.mjs
Normal file
@@ -0,0 +1,187 @@
|
||||
import { chromium } from "playwright";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const getArg = (name, fallback) => {
|
||||
const idx = args.indexOf(`--${name}`);
|
||||
if (idx === -1 || idx + 1 >= args.length) return fallback;
|
||||
return args[idx + 1];
|
||||
};
|
||||
|
||||
const baseUrl = getArg("base", "http://localhost:8090/reihen");
|
||||
const workers = Number.parseInt(getArg("workers", "20"), 10);
|
||||
const steps = Number.parseInt(getArg("steps", "200"), 10);
|
||||
const minDelay = Number.parseInt(getArg("minDelay", "2000"), 10);
|
||||
const maxDelay = Number.parseInt(getArg("maxDelay", "30000"), 10);
|
||||
const timeout = Number.parseInt(getArg("timeout", "10000"), 10);
|
||||
const startStagger = Number.parseInt(getArg("startStagger", "500"), 10);
|
||||
const typeChance = Number.parseFloat(getArg("typeChance", "0.8"));
|
||||
const enterChance = Number.parseFloat(getArg("enterChance", "0.35"));
|
||||
const postChance = Number.parseFloat(getArg("postChance", "0.15"));
|
||||
const minTypeLen = Number.parseInt(getArg("minTypeLen", "3"), 10);
|
||||
const maxTypeLen = Number.parseInt(getArg("maxTypeLen", "5"), 10);
|
||||
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
const rand = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
const randomText = (minLen, maxLen) => {
|
||||
const chars = "abcdefghijklmnopqrstuvwxyzäöüß ";
|
||||
const len = rand(minLen, maxLen);
|
||||
let out = "";
|
||||
for (let i = 0; i < len; i += 1) {
|
||||
out += chars[Math.floor(Math.random() * chars.length)];
|
||||
}
|
||||
return out.trim() || "test";
|
||||
};
|
||||
|
||||
const pickAndClick = async (page) => {
|
||||
return page.evaluate(() => {
|
||||
const origin = window.location.origin;
|
||||
const links = Array.from(document.querySelectorAll("a[href]"));
|
||||
const candidates = links.filter((a) => {
|
||||
const raw = a.getAttribute("href") || "";
|
||||
if (!raw || raw.startsWith("#")) return false;
|
||||
if (raw.startsWith("javascript:")) return false;
|
||||
if (raw.startsWith("mailto:")) return false;
|
||||
if (a.getAttribute("target") === "_blank") return false;
|
||||
if (a.getAttribute("aria-disabled") === "true") return false;
|
||||
const url = new URL(raw, window.location.href);
|
||||
const path = `${url.pathname}${url.search}`.toLowerCase();
|
||||
if (path.includes("logout") || path.includes("delete") || path.includes("/login")) return false;
|
||||
if (url.origin !== origin) return false;
|
||||
return true;
|
||||
});
|
||||
if (!candidates.length) return null;
|
||||
const link = candidates[Math.floor(Math.random() * candidates.length)];
|
||||
const href = link.href;
|
||||
const clickerId = `clicker-${Math.random().toString(36).slice(2)}`;
|
||||
link.setAttribute("data-clicker-id", clickerId);
|
||||
return { href, clickerId };
|
||||
});
|
||||
};
|
||||
|
||||
const pickAndFocusInput = async (page) => {
|
||||
return page.evaluate(() => {
|
||||
const candidates = Array.from(document.querySelectorAll("input, textarea")).filter((el) => {
|
||||
if (!(el instanceof HTMLElement)) return false;
|
||||
if (el.closest("[aria-hidden='true']")) return false;
|
||||
if (el.closest("[data-htmx-busy='true']")) return false;
|
||||
if (el.hasAttribute("disabled") || el.hasAttribute("readonly")) return false;
|
||||
if (el instanceof HTMLInputElement) {
|
||||
const type = (el.type || "text").toLowerCase();
|
||||
if (["hidden", "password", "checkbox", "radio", "file", "submit", "button"].includes(type)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (el.offsetParent === null) return false;
|
||||
return true;
|
||||
});
|
||||
if (!candidates.length) return null;
|
||||
const el = candidates[Math.floor(Math.random() * candidates.length)];
|
||||
el.focus();
|
||||
return {
|
||||
tag: el.tagName.toLowerCase(),
|
||||
id: el.id || "",
|
||||
name: el.getAttribute("name") || "",
|
||||
placeholder: el.getAttribute("placeholder") || "",
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const submitRandomPostForm = async (page) => {
|
||||
return page.evaluate(() => {
|
||||
const forms = Array.from(document.querySelectorAll("form")).filter((form) => {
|
||||
if (!(form instanceof HTMLFormElement)) return false;
|
||||
const method = (form.getAttribute("method") || "get").toLowerCase();
|
||||
if (method !== "post") return false;
|
||||
const action = (form.getAttribute("action") || "").toLowerCase();
|
||||
if (action.includes("/login") || action.includes("logout") || action.includes("delete")) return false;
|
||||
if (form.hasAttribute("disabled")) return false;
|
||||
if (form.closest("[aria-hidden='true']")) return false;
|
||||
if (form.closest("[data-htmx-busy='true']")) return false;
|
||||
return true;
|
||||
});
|
||||
if (!forms.length) return null;
|
||||
const form = forms[Math.floor(Math.random() * forms.length)];
|
||||
const submit = form.querySelector("button[type='submit'], input[type='submit']");
|
||||
if (submit instanceof HTMLElement) {
|
||||
submit.click();
|
||||
} else {
|
||||
form.requestSubmit();
|
||||
}
|
||||
return form.getAttribute("action") || window.location.href;
|
||||
});
|
||||
};
|
||||
|
||||
const runWorker = async (id, browser, delayMs) => {
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
page.setDefaultTimeout(timeout);
|
||||
const label = `worker-${id}`;
|
||||
try {
|
||||
if (delayMs > 0) {
|
||||
await sleep(delayMs);
|
||||
}
|
||||
await page.goto(baseUrl, { waitUntil: "domcontentloaded" });
|
||||
for (let i = 0; i < steps; i += 1) {
|
||||
const clickInfo = await pickAndClick(page);
|
||||
if (clickInfo) {
|
||||
const selector = `[data-clicker-id="${clickInfo.clickerId}"]`;
|
||||
await page.click(selector).catch(() => {});
|
||||
console.log(`${label}: click ${clickInfo.href}`);
|
||||
} else {
|
||||
console.log(`${label}: no candidates`);
|
||||
}
|
||||
await page.waitForLoadState("domcontentloaded").catch(() => {});
|
||||
await page.waitForTimeout(200);
|
||||
const currentUrl = page.url();
|
||||
if (currentUrl) {
|
||||
console.log(`${label}: at ${currentUrl}`);
|
||||
}
|
||||
if (Math.random() < typeChance) {
|
||||
const focused = await pickAndFocusInput(page);
|
||||
if (focused) {
|
||||
const text = randomText(minTypeLen, maxTypeLen);
|
||||
await page.keyboard.type(text, { delay: rand(10, 80) });
|
||||
console.log(
|
||||
`${label}: type \"${text}\" into ${focused.tag}${focused.id ? `#${focused.id}` : ""}${focused.name ? `[name=\"${focused.name}\"]` : ""}`,
|
||||
);
|
||||
if (focused.tag !== "textarea" && Math.random() < enterChance) {
|
||||
await page.keyboard.press("Enter");
|
||||
console.log(`${label}: press Enter`);
|
||||
}
|
||||
} else {
|
||||
console.log(`${label}: no inputs to type`);
|
||||
}
|
||||
} else {
|
||||
console.log(`${label}: skip typing`);
|
||||
}
|
||||
if (Math.random() < postChance) {
|
||||
const action = await submitRandomPostForm(page);
|
||||
if (action) {
|
||||
console.log(`${label}: submit POST ${action}`);
|
||||
} else {
|
||||
console.log(`${label}: no POST forms`);
|
||||
}
|
||||
}
|
||||
await sleep(rand(minDelay, maxDelay));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${label}: error`, error?.message || error);
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
try {
|
||||
const runs = Array.from({ length: workers }, (_, idx) => runWorker(idx + 1, browser, idx * startStagger));
|
||||
await Promise.all(runs);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
};
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user