Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
592 changes: 398 additions & 194 deletions tests/behat/features/SpecialCase1.feature

Large diffs are not rendered by default.

258 changes: 258 additions & 0 deletions tests/behat/features/bootstrap/FeatureContext.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<?php

use Behat\Behat\Hook\Scope\AfterStepScope;
use Behat\Gherkin\Node\TableNode;
use Behat\MinkExtension\Context\MinkContext;
use Behat\Testwork\Tester\Result\TestResult;

/**
* Features context. (MinkContext extends BehatContext)
Expand Down Expand Up @@ -547,6 +549,25 @@ public function waitForThePageToBeLoadedWhenReady()
$this->getSession()->wait(9000, "document.readyState === 'complete'");
}

/**
* @When /^I wait for the element "([^"]*)" to appear$/
*/
public function iWaitForElementToAppear($selector): void
{
$escaped = addslashes($selector);
$this->getSession()->wait(10000, "document.querySelector('{$escaped}') !== null");
}

/**
* @When /^I wait up to 20 seconds for the element "([^"]*)" to appear$/
*/
public function iWait20SecondsForElementToAppear($selector): void
{
$escaped = addslashes($selector);
$this->getSession()->wait(20000, "document.querySelector('{$escaped}') !== null");
}


/**
* @When /^(?:|I )wait one minute for the page to be loaded$/
*/
Expand Down Expand Up @@ -779,6 +800,47 @@ public function iClickTheElement($selector)
$element->click();
}

/**
* Check all checkboxes whose name contains the given partial name.
* Useful for checkbox groups like form[show_tabs][] where IDs are dynamic.
*
* @Then I check all checkboxes with name containing :partialName
*/
public function iCheckAllCheckboxesWithNameContaining($partialName)
{
$js = <<<JS
(function() {
var checkboxes = document.querySelectorAll('input[type="checkbox"][name*="$partialName"]');
checkboxes.forEach(function(cb) { if (!cb.checked) cb.click(); });
return checkboxes.length;
})();
JS;
$count = $this->getSession()->evaluateScript($js);
if ($count === 0) {
throw new \Exception(sprintf('No checkboxes found with name containing "%s"', $partialName));
}
}

/**
* Uncheck all checkboxes whose name contains the given partial name.
*
* @Then I uncheck all checkboxes with name containing :partialName
*/
public function iUncheckAllCheckboxesWithNameContaining($partialName)
{
$js = <<<JS
(function() {
var checkboxes = document.querySelectorAll('input[type="checkbox"][name*="$partialName"]');
checkboxes.forEach(function(cb) { if (cb.checked) cb.click(); });
return checkboxes.length;
})();
JS;
$count = $this->getSession()->evaluateScript($js);
if ($count === 0) {
throw new \Exception(sprintf('No checkboxes found with name containing "%s"', $partialName));
}
}

/**
* @Then /^(?:I see|I should see|And I see)\s+"?([^\"]+)"?\s+in the element "([^\"]+)"$/
*/
Expand Down Expand Up @@ -849,6 +911,202 @@ public function zoomOutMax()
return true;
}

/**
* @AfterStep
*
* When a step fails, dump the full HTML of the page and a form-summary
* into tests/behat/behat_debug/ so Claude (or a developer) can analyse
* the real state of the page and find the correct selectors.
*/
public function dumpHtmlOnFailure(AfterStepScope $scope): void
{
// Only act on failed steps
if ($scope->getTestResult()->getResultCode() !== TestResult::FAILED) {
return;
}

try {
$session = $this->getSession();
$page = $session->getPage();

// Try to get the RENDERED DOM (after JavaScript execution) via Selenium/ChromeDriver.
// This captures the final state including Vue.js/PrimeVue dynamic components.
// Falls back to getContent() (server-side HTML source) if JS evaluation fails.
try {
$driver = $session->getDriver();
$html = $driver->evaluateScript('return document.documentElement.outerHTML');
} catch (\Exception $jsEx) {
$html = $page->getContent();
}

if (empty($html)) {
$html = $page->getContent();
}
} catch (\Exception $e) {
// If we can't even get the page content, bail out silently
return;
}

// Build output directory
$debugDir = __DIR__ . '/../../behat_debug';
if (!is_dir($debugDir)) {
mkdir($debugDir, 0777, true);
}

// Build a unique filename from scenario + step line
$feature = basename($scope->getFeature()->getFile(), '.feature');
$line = $scope->getStep()->getLine();
$timestamp = date('Ymd_His');
$baseName = "{$feature}_line{$line}_{$timestamp}";

// --- 1) Full HTML dump ---
file_put_contents("{$debugDir}/{$baseName}_full.html", $html);

// --- 2) Form-summary: extract all form elements so we can see
// the real field names, types, ids, options, etc. ---
$summary = $this->extractFormSummary($html, $session);
file_put_contents("{$debugDir}/{$baseName}_form_summary.txt", $summary);

// --- 3) Current URL ---
try {
$url = $session->getCurrentUrl();
} catch (\Exception $e) {
$url = '(unable to retrieve URL)';
}

// --- 4) Meta file with context ---
$stepText = $scope->getStep()->getText();
$meta = "BEHAT DEBUG — Step failure\n";
$meta .= "==========================\n\n";
$meta .= "Feature : {$scope->getFeature()->getFile()}\n";
$meta .= "Step : {$stepText}\n";
$meta .= "Line : {$line}\n";
$meta .= "URL : {$url}\n";
$meta .= "Time : {$timestamp}\n\n";
$meta .= "Files generated:\n";
$meta .= " - {$baseName}_full.html (complete page HTML)\n";
$meta .= " - {$baseName}_form_summary.txt (extracted form fields)\n";
file_put_contents("{$debugDir}/{$baseName}_meta.txt", $meta);
}

/**
* Parse the HTML and extract a human-readable summary of all form fields.
* This includes: inputs, selects (with their options), textareas, buttons.
*/
private function extractFormSummary(string $html, $session): string
{
$lines = [];
$lines[] = "=== FORM FIELDS SUMMARY ===";
$lines[] = "URL: " . (method_exists($session, 'getCurrentUrl') ? $session->getCurrentUrl() : 'N/A');
$lines[] = str_repeat('=', 60);
$lines[] = '';

// Use DOMDocument to parse
$dom = new \DOMDocument();
@$dom->loadHTML($html, LIBXML_NOERROR | LIBXML_NOWARNING);
$xpath = new \DOMXPath($dom);

// --- INPUTS ---
$inputs = $xpath->query('//input');
if ($inputs->length > 0) {
$lines[] = "--- INPUT FIELDS ({$inputs->length}) ---";
foreach ($inputs as $input) {
$type = $input->getAttribute('type') ?: 'text';
$name = $input->getAttribute('name');
$id = $input->getAttribute('id');
$value = $input->getAttribute('value');
$checked = $input->getAttribute('checked') ? ' [CHECKED]' : '';
$disabled = $input->getAttribute('disabled') ? ' [DISABLED]' : '';
$placeholder = $input->getAttribute('placeholder');

$info = " <input type=\"{$type}\"";
if ($name) $info .= " name=\"{$name}\"";
if ($id) $info .= " id=\"{$id}\"";
if ($value && strlen($value) < 100) $info .= " value=\"{$value}\"";
if ($placeholder) $info .= " placeholder=\"{$placeholder}\"";
$info .= "{$checked}{$disabled}>";
$lines[] = $info;
}
$lines[] = '';
}

// --- SELECTS ---
$selects = $xpath->query('//select');
if ($selects->length > 0) {
$lines[] = "--- SELECT FIELDS ({$selects->length}) ---";
foreach ($selects as $select) {
$name = $select->getAttribute('name');
$id = $select->getAttribute('id');
$multiple = $select->getAttribute('multiple') ? ' [MULTIPLE]' : '';

$lines[] = " <select name=\"{$name}\" id=\"{$id}\"{$multiple}>";

$options = $xpath->query('.//option', $select);
foreach ($options as $option) {
$optValue = $option->getAttribute('value');
$optText = trim($option->textContent);
$selected = $option->getAttribute('selected') ? ' *SELECTED*' : '';
$lines[] = " <option value=\"{$optValue}\"{$selected}>{$optText}</option>";
}
$lines[] = " </select>";
$lines[] = '';
}
}

// --- TEXTAREAS ---
$textareas = $xpath->query('//textarea');
if ($textareas->length > 0) {
$lines[] = "--- TEXTAREA FIELDS ({$textareas->length}) ---";
foreach ($textareas as $ta) {
$name = $ta->getAttribute('name');
$id = $ta->getAttribute('id');
$content = substr(trim($ta->textContent), 0, 200);
$lines[] = " <textarea name=\"{$name}\" id=\"{$id}\">{$content}...</textarea>";
}
$lines[] = '';
}

// --- BUTTONS ---
$buttons = $xpath->query('//button | //input[@type="submit"] | //input[@type="button"]');
if ($buttons->length > 0) {
$lines[] = "--- BUTTONS ({$buttons->length}) ---";
foreach ($buttons as $btn) {
$tag = $btn->nodeName;
$type = $btn->getAttribute('type') ?: '';
$name = $btn->getAttribute('name');
$id = $btn->getAttribute('id');
$text = trim($btn->textContent);
$classes = $btn->getAttribute('class');

$info = " <{$tag}";
if ($type) $info .= " type=\"{$type}\"";
if ($name) $info .= " name=\"{$name}\"";
if ($id) $info .= " id=\"{$id}\"";
if ($classes) $info .= " class=\"{$classes}\"";
$info .= ">";
if ($text) $info .= "{$text}</{$tag}>";
$lines[] = $info;
}
$lines[] = '';
}

// --- LABELS (useful to map labels to field IDs) ---
$labels = $xpath->query('//label[@for]');
if ($labels->length > 0) {
$lines[] = "--- LABELS WITH 'for' ATTRIBUTE ({$labels->length}) ---";
foreach ($labels as $label) {
$for = $label->getAttribute('for');
$text = trim($label->textContent);
if ($text) {
$lines[] = " <label for=\"{$for}\">{$text}</label>";
}
}
$lines[] = '';
}

return implode("\n", $lines);
}

public function visit($page): void
{
parent::visit($page);
Expand Down
77 changes: 77 additions & 0 deletions tests/behat/read_debug.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/bin/bash
# ============================================================
# read_debug.sh — Read the latest Behat debug dump
#
# Usage:
# ./read_debug.sh → shows the latest meta + form_summary
# ./read_debug.sh full → shows the latest full HTML
# ./read_debug.sh all → shows meta + form_summary + full HTML
# ./read_debug.sh clean → delete all debug files
# ============================================================

DEBUG_DIR="$(dirname "$0")/behat_debug"

if [ ! -d "$DEBUG_DIR" ]; then
echo "No behat_debug/ directory found. No failures captured yet."
exit 0
fi

case "${1:-summary}" in
clean)
rm -f "$DEBUG_DIR"/*.html "$DEBUG_DIR"/*.txt
echo "Debug files cleaned."
exit 0
;;
full)
LATEST=$(ls -t "$DEBUG_DIR"/*_full.html 2>/dev/null | head -1)
if [ -z "$LATEST" ]; then
echo "No full HTML dump found."
exit 0
fi
echo "=== LATEST FULL HTML DUMP: $(basename "$LATEST") ==="
cat "$LATEST"
;;
all)
# Meta
LATEST_META=$(ls -t "$DEBUG_DIR"/*_meta.txt 2>/dev/null | head -1)
if [ -n "$LATEST_META" ]; then
echo ""
echo "=== META ==="
cat "$LATEST_META"
fi
echo ""
# Form summary
LATEST_SUMMARY=$(ls -t "$DEBUG_DIR"/*_form_summary.txt 2>/dev/null | head -1)
if [ -n "$LATEST_SUMMARY" ]; then
echo ""
echo "=== FORM SUMMARY ==="
cat "$LATEST_SUMMARY"
fi
echo ""
# Full HTML (first 500 lines to avoid flooding)
LATEST_FULL=$(ls -t "$DEBUG_DIR"/*_full.html 2>/dev/null | head -1)
if [ -n "$LATEST_FULL" ]; then
echo ""
echo "=== FULL HTML (first 500 lines) ==="
head -500 "$LATEST_FULL"
fi
;;
summary|*)
# Meta
LATEST_META=$(ls -t "$DEBUG_DIR"/*_meta.txt 2>/dev/null | head -1)
if [ -n "$LATEST_META" ]; then
echo "=== META ==="
cat "$LATEST_META"
echo ""
fi
# Form summary
LATEST_SUMMARY=$(ls -t "$DEBUG_DIR"/*_form_summary.txt 2>/dev/null | head -1)
if [ -n "$LATEST_SUMMARY" ]; then
echo "=== FORM SUMMARY ==="
cat "$LATEST_SUMMARY"
else
echo "No form summary found."
fi
;;
esac

Loading
Loading