import { addMatchImageSnapshotCommand } from '@simonsmith/cypress-image-snapshot/command';
import { registerMonkeyCommands } from './monkey.js';
addMatchImageSnapshotCommand({
failureThreshold: 0.1,
failureThresholdType: 'percent'
});
registerMonkeyCommands();
Cypress.Commands.add('login', () => {
cy.env(['USERNAME', 'PASSWORD']).then(({ USERNAME: username, PASSWORD: password }) => {
cy.visit('/');
cy.get('[name="log"]').type(username, { log: false });
cy.get('[name="pwd"]').type(password, { log: false });
cy.get('[name="wp-submit"]').click();
cy.visit('/');
});
});
beforeEach(() => {});
Cypress.Commands.add('restore', date => {
cy.exec(`./db.sh restore ${date}`, {
timeout: 120000
});
});
Cypress.Commands.add('replay', harFixtureName => {
const harFixturePath = harFixtureName + '.har';
let snapshotPrefix = 'har' + harFixturePath,
baseUrl = Cypress.config('baseUrl'),
waitAfterNavigate = 800,
waitAfterPost = 500,
snapshotOptions = {};
cy.readFile('recordings/' + harFixturePath).then(raw => {
const har = typeof raw === 'string' ? JSON.parse(raw) : raw;
const entries = har.log.entries.sort((a, b) => new Date(a.startedDateTime) - new Date(b.startedDateTime));
let snapshotIndex = 1;
const snap = label => {
/*
let name = `${snapshotPrefix}-${String(snapshotIndex++).padStart(3, '0')}-${label}`
.replace(/[^a-zA-Z0-9-_]/g, '_')
.substring(0, 80);
*/
let name = '_' + snapshotIndex++;
cy.matchImageSnapshot(name, snapshotOptions);
};
// Klassifiziere jeden Entry
const steps = entries.map(entry => {
const url = new URL(entry.request.url);
const method = entry.request.method;
const accept = (entry.request.headers || []).find(h => h.name.toLowerCase() === 'accept');
const isDocument =
entry._resourceType === 'document' || (method === 'GET' && accept?.value?.includes('text/html'));
return { entry, method, url, isDocument };
});
steps.forEach(step => {
const { entry, method, url, isDocument } = step;
const path = url.pathname + url.search;
if (isDocument && method === 'GET') {
// ====== Seitennavigation ======
const targetUrl = baseUrl ? path : entry.request.url;
cy.visit(targetUrl, { failOnStatusCode: false });
cy.wait(waitAfterNavigate);
snap(path.replace(/\//g, '_'));
} else if (method === 'POST' || method === 'PUT' || method === 'PATCH' || method === 'DELETE') {
// ====== Schreibende Requests → echt ausführen ======
const headers = Object.fromEntries(
(entry.request.headers || [])
.filter(
h =>
!['host', 'content-length', 'connection', 'accept-encoding'].includes(
h.name.toLowerCase()
)
)
.map(h => [h.name, h.value])
);
let body = entry.request.postData?.text || undefined;
const mimeType = entry.request.postData?.mimeType || '';
// JSON body parsen
if (mimeType.includes('application/json') && body) {
try {
body = JSON.parse(body);
} catch {}
}
const targetUrl = baseUrl ? `${baseUrl}${path}` : entry.request.url;
cy.request({
method,
url: targetUrl,
body,
headers,
failOnStatusCode: false,
followRedirect: false
}).then(response => {
const contentType = response.headers['content-type'] || '';
const location = response.headers['location'];
if (location) {
// follow redirect
cy.visit(location, { failOnStatusCode: false });
cy.wait(waitAfterNavigate);
} else if (contentType.includes('text/html')) {
// render returned HTML directly
cy.document().then(doc => {
doc.open();
doc.write(response.body);
doc.close();
});
cy.wait(waitAfterNavigate);
} else {
// reload to reflect side effects
cy.reload();
cy.wait(waitAfterNavigate);
}
});
cy.wait(waitAfterPost);
snap(`${method}_${path.replace(/\//g, '_')}`);
}
// API-GETs (XHR/fetch) → ignorieren, passieren automatisch beim visit/reload
});
});
});
const MONKEY_DANGEROUS_TOKENS = [
'delete',
'remove',
'destroy',
'truncate',
'drop',
'logout',
'signout',
'abmelden',
'loeschen',
'loschen',
'loesch',
'lorsch',
'entfernen',
'kuend',
'storn',
'archiv',
'reset',
'revoke'
];
const MONKEY_SELECTORS = {
click: ['a[href]', 'button', '[role="button"]', 'input[type="submit"]', 'input[type="button"]', 'summary'].join(
','
),
type: [
'textarea',
'input:not([type])',
'input[type="text"]',
'input[type="search"]',
'input[type="email"]',
'input[type="url"]',
'input[type="tel"]',
'input[type="number"]',
'input[type="date"]',
'input[type="datetime-local"]',
'input[type="month"]',
'input[type="time"]',
'input[type="week"]'
].join(','),
toggle: 'input[type="checkbox"], input[type="radio"]',
select: 'select'
};
const MONKEY_DEFAULTS = {
seed: '20260309',
steps: 50,
settleMs: 250,
startPath: '/',
allowBack: true,
screenshotEachStep: true,
blackout: [],
recordFile: 'recordings/monkey/2026-03-08-20260309.json',
snapshotPrefix: 'monkey/2026-03-08/20260309',
weights: {
click: 5,
type: 3,
select: 2,
toggle: 2,
back: 1
}
};
const monkeyFormatDuration = milliseconds => {
const totalSeconds = Math.max(0, Math.round(milliseconds / 1000));
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${minutes}m ${String(seconds).padStart(2, '0')}s`;
};
const monkeyPrintStatus = message => {
Cypress.log({
name: 'monkey',
message
});
return cy.task('log', message, { log: false });
};
const monkeyDescribeStep = step => {
const label = (step.label || '').trim();
if (step.action === 'type') {
return `${step.action} ${step.tagName}${step.name ? `:${step.name}` : ''} = ${String(step.value || '').slice(0, 40)}`;
}
if (step.action === 'select') {
return `${step.action} ${step.tagName}${step.name ? `:${step.name}` : ''} = ${String(step.value || '').slice(0, 40)}`;
}
if (step.action === 'visit') {
return `${step.action} ${step.target}`;
}
if (step.action === 'back') {
return 'back';
}
return `${step.action} ${step.tagName}${label ? ` ${label.slice(0, 40)}` : ''}`;
};
const createMonkeySeed = seedValue => {
const input = String(seedValue ?? Date.now());
let state = 0;
for (let index = 0; index < input.length; index++) {
state = (state * 31 + input.charCodeAt(index)) >>> 0;
}
return state || 1;
};
const createMonkeyRandom = seedValue => {
let state = createMonkeySeed(seedValue);
return () => {
state = (state * 1664525 + 1013904223) >>> 0;
return state / 4294967296;
};
};
const monkeyPickIndex = (length, nextRandom) => {
if (length <= 1) {
return 0;
}
return Math.min(length - 1, Math.floor(nextRandom() * length));
};
const monkeyBuildScreenshotName = (prefix, stepIndex, action) => {
return [prefix, `${String(stepIndex + 1).padStart(4, '0')}-${action}`].join('/');
};
const monkeyGetElementText = element => {
return [
element.getAttribute('aria-label'),
element.getAttribute('title'),
element.getAttribute('name'),
element.getAttribute('value'),
element.textContent
]
.filter(Boolean)
.join(' ')
.replace(/\s+/g, ' ')
.trim();
};
const monkeyGetFieldType = element => {
if ((element.tagName || '').toLowerCase() === 'textarea') {
return 'textarea';
}
return String(element.type || element.getAttribute('type') || 'text').toLowerCase();
};
const monkeyIsVisible = element => {
if (!(element instanceof element.ownerDocument.defaultView.HTMLElement)) {
return false;
}
const rect = element.getBoundingClientRect();
const style = element.ownerDocument.defaultView.getComputedStyle(element);
if (rect.width <= 0 || rect.height <= 0) {
return false;
}
if (style.visibility === 'hidden' || style.display === 'none' || style.pointerEvents === 'none') {
return false;
}
return true;
};
const monkeyIsDisabled = element => {
return element.matches(':disabled') || element.getAttribute('aria-disabled') === 'true';
};
const monkeyIsDangerous = element => {
const text = monkeyGetElementText(element).toLowerCase();
const href = (element.getAttribute('href') || '').toLowerCase();
const onclick = (element.getAttribute('onclick') || '').toLowerCase();
const haystack = `${text} ${href} ${onclick}`;
if (href.startsWith('mailto:') || href.startsWith('tel:') || href.startsWith('javascript:')) {
return true;
}
return MONKEY_DANGEROUS_TOKENS.some(token => haystack.includes(token));
};
const monkeyGetCandidates = (doc, group) => {
return [...doc.querySelectorAll(MONKEY_SELECTORS[group] || '')].filter(element => {
if (!monkeyIsVisible(element) || monkeyIsDisabled(element) || monkeyIsDangerous(element)) {
return false;
}
if (group === 'select') {
return [...element.options].some(option => !option.disabled && option.value !== '');
}
return true;
});
};
const monkeyPickWeightedAction = (groups, settings, nextRandom) => {
const weightedActions = [];
if (groups.click.length > 0) {
weightedActions.push({ action: 'click', weight: settings.weights.click });
}
if (groups.type.length > 0) {
weightedActions.push({ action: 'type', weight: settings.weights.type });
}
if (groups.select.length > 0) {
weightedActions.push({ action: 'select', weight: settings.weights.select });
}
if (groups.toggle.length > 0) {
weightedActions.push({ action: 'toggle', weight: settings.weights.toggle });
}
if (settings.allowBack) {
weightedActions.push({ action: 'back', weight: settings.weights.back });
}
const totalWeight = weightedActions.reduce((sum, item) => sum + item.weight, 0);
if (totalWeight === 0) {
return 'visit';
}
let value = nextRandom() * totalWeight;
for (const item of weightedActions) {
value -= item.weight;
if (value <= 0) {
return item.action;
}
}
return weightedActions[weightedActions.length - 1].action;
};
const monkeyCreateTypedValue = (element, nextRandom, stepIndex) => {
const type = monkeyGetFieldType(element);
const serial = Math.floor(nextRandom() * 100000);
const day = String((stepIndex % 28) + 1).padStart(2, '0');
const minute = String(serial % 60).padStart(2, '0');
const week = String((stepIndex % 52) + 1).padStart(2, '0');
if (type === 'email') {
return `monkey.${stepIndex}.${serial}@example.test`;
}
if (type === 'url') {
return `https://example.test/${serial}`;
}
if (type === 'tel') {
return `089${String(serial).padStart(6, '0').slice(0, 6)}`;
}
if (type === 'number') {
return String(Math.floor(nextRandom() * 10000));
}
if (type === 'date') {
return `2026-03-${day}`;
}
if (type === 'datetime-local') {
return `2026-03-${day}T12:${minute}`;
}
if (type === 'month') {
return '2026-03';
}
if (type === 'time') {
return `12:${minute}`;
}
if (type === 'week') {
return `2026-W${week}`;
}
const samples = [
'alpha test',
'beta 42',
'gamma /?foo=bar',
'delta+check@example.test',
'epsilon <tag>',
'zeta & value',
'eta 2026-03-09'
];
return `${samples[monkeyPickIndex(samples.length, nextRandom)]} ${serial}`;
};
const monkeyNormalizeTypedValue = (element, rawValue) => {
const type = monkeyGetFieldType(element);
const value = String(rawValue ?? '');
if (type === 'date') {
return /^\d{4}-\d{2}-\d{2}$/.test(value) ? value : '2026-03-09';
}
if (type === 'datetime-local') {
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(value) ? value : '2026-03-09T12:00';
}
if (type === 'month') {
return /^\d{4}-\d{2}$/.test(value) ? value : '2026-03';
}
if (type === 'time') {
return /^\d{2}:\d{2}$/.test(value) ? value : '12:00';
}
if (type === 'week') {
return /^\d{4}-W\d{2}$/.test(value) ? value : '2026-W10';
}
return value;
};
const monkeyCreateStep = (doc, settings, nextRandom, stepIndex) => {
const groups = {
click: monkeyGetCandidates(doc, 'click'),
type: monkeyGetCandidates(doc, 'type'),
toggle: monkeyGetCandidates(doc, 'toggle'),
select: monkeyGetCandidates(doc, 'select')
};
const action = monkeyPickWeightedAction(groups, settings, nextRandom);
const currentUrl = `${doc.location.pathname}${doc.location.search}`;
if (action === 'visit') {
return {
action: 'visit',
target: settings.startPath,
url: currentUrl
};
}
if (action === 'back') {
return {
action: 'back',
url: currentUrl
};
}
const candidates = groups[action];
if (!candidates || candidates.length === 0) {
return {
action: 'visit',
target: settings.startPath,
url: currentUrl
};
}
const index = monkeyPickIndex(candidates.length, nextRandom);
const element = candidates[index];
const step = {
action,
index,
url: currentUrl,
label: monkeyGetElementText(element).slice(0, 120),
tagName: element.tagName.toLowerCase(),
name: element.getAttribute('name') || null
};
if (action === 'type') {
step.value = monkeyNormalizeTypedValue(element, monkeyCreateTypedValue(element, nextRandom, stepIndex));
}
if (action === 'select') {
const options = [...element.options].filter(option => !option.disabled && option.value !== '');
step.value = options[monkeyPickIndex(options.length, nextRandom)].value;
}
return step;
};
const monkeySaveRun = payload => {
return cy.writeFile(payload.recordFile, payload.data, {
log: false,
timeout: 120000
});
};
const monkeyAssertPage = () => {
return cy.document({ log: false }).then(doc => {
const bodyText = (doc.body?.innerText || '').toLowerCase();
const html = doc.documentElement.innerHTML.toLowerCase();
expect(doc.body, 'body').to.exist;
expect(bodyText, 'fatal error').not.to.include('fatal error');
expect(bodyText, 'critical error').not.to.include('critical error on this website');
expect(bodyText, 'uncaught').not.to.include('uncaught');
expect(html, 'php warning').not.to.include('warning:</b>');
expect(html, 'php notice').not.to.include('notice:</b>');
expect(html, 'php parse error').not.to.include('parse error');
});
};
const monkeyExecuteStep = (step, settings) => {
if (step.action === 'visit') {
return cy.visit(step.target, { failOnStatusCode: false });
}
if (step.action === 'back') {
return cy.go('back');
}
return cy.document({ log: false }).then(doc => {
const candidates = monkeyGetCandidates(doc, step.action);
const element = candidates[step.index];
if (!element) {
return cy.visit(settings.startPath, { failOnStatusCode: false });
}
const chain = cy.wrap(element, { log: false }).scrollIntoView({ log: false });
if (step.action === 'click') {
return chain.click({ force: true, log: false });
}
if (step.action === 'toggle') {
return chain.click({ force: true, log: false });
}
if (step.action === 'type') {
const value = monkeyNormalizeTypedValue(element, step.value);
return chain
.focus({ log: false })
.invoke('val', value)
.trigger('input', { force: true, log: false })
.trigger('change', { force: true, log: false })
.trigger('keyup', { force: true, log: false })
.blur({ log: false });
}
if (step.action === 'select') {
return chain.select(step.value, { force: true, log: false });
}
});
};
export const registerMonkeyCommands = () => {
const createMonkeyRun = settings => {
const nextRandom = createMonkeyRandom(settings.seed);
const startedAt = new Date().toISOString();
const startedAtMs = Date.now();
const steps = [];
const meta = {
seed: String(settings.seed),
steps: settings.steps,
startPath: settings.startPath,
startedAt
};
const runStep = stepIndex => {
if (stepIndex >= settings.steps) {
return monkeySaveRun({
recordFile: settings.recordFile,
data: {
meta,
steps
}
}).then(() => {
const duration = monkeyFormatDuration(Date.now() - startedAtMs);
return monkeyPrintStatus(
`Monkey finished: mode=create, steps=${settings.steps}, duration=${duration}, seed=${settings.seed}`
).then(() => cy.wrap(steps, { log: false }));
});
}
return cy.document({ log: false }).then(doc => {
const step = monkeyCreateStep(doc, settings, nextRandom, stepIndex);
steps.push(step);
return monkeyPrintStatus(
`Monkey step ${stepIndex + 1}/${settings.steps}: ${monkeyDescribeStep(step)}`
).then(() => {
return monkeySaveRun({
recordFile: settings.recordFile,
data: {
meta,
steps
}
}).then(() => {
return monkeyExecuteStep(step, settings)
.then(() => cy.wait(settings.settleMs, { log: false }))
.then(() => monkeyAssertPage())
.then(() => {
if (settings.screenshotEachStep !== true) {
return;
}
return cy.matchImageSnapshot(
monkeyBuildScreenshotName(settings.snapshotPrefix, stepIndex, step.action),
{
blackout: settings.blackout
}
);
})
.then(() => runStep(stepIndex + 1));
});
});
});
};
const estimatedMinimumDuration = monkeyFormatDuration(settings.steps * settings.settleMs);
return monkeyPrintStatus(
`Monkey started: mode=create, steps=${settings.steps}, settleMs=${settings.settleMs}, estimated-min=${estimatedMinimumDuration}, seed=${settings.seed}`
).then(() => runStep(0));
};
const replayMonkeyRun = recordFile => {
return cy.readFile(recordFile).then(run => {
const settings = Cypress._.merge({}, MONKEY_DEFAULTS, run.meta || {}, { recordFile });
const startedAtMs = Date.now();
const steps = run.steps || [];
const replayStep = stepIndex => {
if (stepIndex >= steps.length) {
const duration = monkeyFormatDuration(Date.now() - startedAtMs);
return monkeyPrintStatus(
`Monkey finished: mode=replay, steps=${steps.length}, duration=${duration}, seed=${settings.seed}`
).then(() => cy.wrap(steps, { log: false }));
}
return monkeyPrintStatus(
`Monkey step ${stepIndex + 1}/${steps.length}: ${monkeyDescribeStep(steps[stepIndex])}`
).then(() => {
return monkeyExecuteStep(steps[stepIndex], settings)
.then(() => cy.wait(settings.settleMs, { log: false }))
.then(() => monkeyAssertPage())
.then(() => {
if (settings.screenshotEachStep !== true) {
return;
}
return cy.matchImageSnapshot(
monkeyBuildScreenshotName(settings.snapshotPrefix, stepIndex, steps[stepIndex].action),
{
blackout: settings.blackout
}
);
})
.then(() => replayStep(stepIndex + 1));
});
};
const estimatedMinimumDuration = monkeyFormatDuration(steps.length * settings.settleMs);
return monkeyPrintStatus(
`Monkey started: mode=replay, steps=${steps.length}, settleMs=${settings.settleMs}, estimated-min=${estimatedMinimumDuration}, seed=${settings.seed}`
).then(() => replayStep(0));
});
};
Cypress.Commands.add('runMonkey', () => {
const settings = Cypress._.cloneDeep(MONKEY_DEFAULTS);
return cy.task('fileExists', settings.recordFile, { log: false }).then(exists => {
if (exists) {
return replayMonkeyRun(settings.recordFile);
}
return createMonkeyRun(settings);
});
});
Cypress.Commands.add('replayMonkey', () => {
return replayMonkeyRun(MONKEY_DEFAULTS.recordFile);
});
};