markup (no regression)
* - more than one value drops the outer and links each value instead,
* because anchors may not nest
* - empty rows, and invalid emails, are skipped rather than rendering blanks
* - tel: keeps only digits plus a single LEADING plus
*
* Run: php plugins/a1-tools/tests/contact-info-values.test.php
*
* @package A1_Tools
*/
// ── Minimal WordPress surface ───────────────────────────────────────
namespace {
define( 'ABSPATH', __DIR__ );
function __( $t, $d = null ) {
return $t; }
function esc_html( $t ) {
return htmlspecialchars( (string) $t, ENT_QUOTES ); }
function esc_attr( $t ) {
return htmlspecialchars( (string) $t, ENT_QUOTES ); }
function esc_url( $u ) {
return htmlspecialchars( (string) $u, ENT_QUOTES ); }
function sanitize_email( $e ) {
return trim( (string) $e ); }
function is_email( $e ) {
return (bool) filter_var( $e, FILTER_VALIDATE_EMAIL ); }
function wp_json_encode( $d ) {
return json_encode( $d ); }
}
// ── Minimal Elementor surface ───────────────────────────────────────
namespace Elementor {
class Widget_Base {
public $injected = array();
public function get_settings_for_display() {
return $this->injected; }
}
class Repeater {
private $c = array();
public function add_control( $id, $a ) {
$this->c[ $id ] = $a; }
public function get_controls() {
return $this->c; }
}
class Icons_Manager {
public static function render_icon( $icon, $attrs = array() ) {
echo '';
return true;
}
}
class Controls_Manager {
const TEXT = 'text'; const TEXTAREA = 'textarea'; const SWITCHER = 'switcher';
const ICONS = 'icons'; const REPEATER = 'repeater'; const SLIDER = 'slider';
const COLOR = 'color'; const DIMENSIONS = 'dimensions'; const SELECT = 'select';
const TAB_CONTENT = 'content'; const TAB_STYLE = 'style';
}
}
namespace {
require_once __DIR__ . '/../includes/class-a1-tools-contact-info-widget.php';
/** Site variables the widget reads. */
function a1tools_get_site_variables() {
return array(
'phone_primary' => '(216) 555-0100',
'email_primary' => 'info@example.com',
'full_address' => '1 Main St, Cleveland, OH 44101',
'operating_hours' => array( 'mon' => '8:00 AM - 10:00 PM', 'sat' => '' ),
);
}
$failures = 0;
function check( $label, $cond ) {
global $failures;
if ( $cond ) {
echo " ok $label\n";
return;
}
++$failures;
echo " FAIL $label\n";
}
/**
* Render the widget with the given settings and return its HTML.
*/
function render_with( array $settings ) {
$w = new A1Tools_Contact_Info_Elementor_Widget();
$w->injected = $settings;
$render = new ReflectionMethod( $w, 'render' );
$render->setAccessible( true );
ob_start();
$render->invoke( $w );
return ob_get_clean();
}
$base = array(
'show_phone' => 'yes',
'phone_link' => 'yes',
'phone_title' => 'Phone',
'phone_icon' => array( 'value' => 'fas fa-phone-alt' ),
'show_email' => '',
'show_location' => '',
'show_hours' => '',
'show_license' => '',
);
echo "Contact Info values\n";
// 1. Single linked value → unchanged single-anchor markup.
$html = render_with( $base );
check( 'single value keeps one item-level ', 1 === substr_count( $html, ' array(
array( 'number' => '+1 (888) 555-0199', 'label' => 'Sales', 'link' => 'yes' ),
),
) );
check( 'multi value drops the item-level anchor', false === strpos( $html, 'a1tools-contact-info-item a1tools-info-box-link' ) );
check( 'multi value links each value', 2 === substr_count( $html, '' ) );
check( 'leading plus is preserved', false !== strpos( $html, 'href="tel:+18885550199"' ) );
check( 'label renders with a colon', false !== strpos( $html, 'Sales:' ) );
check( 'additional value is marked additional', false !== strpos( $html, 'a1tools-info-box-value--additional' ) );
// 3. Empty and unlinked rows.
$html = render_with( $base + array(
'phone_extra' => array(
array( 'number' => ' ', 'label' => 'Ghost', 'link' => 'yes' ),
array( 'number' => '216-555-0200', 'label' => '', 'link' => '' ),
),
) );
check( 'empty row is skipped', false === strpos( $html, 'Ghost' ) );
check( 'unlinked extra renders as plain text', false !== strpos( $html, '216-555-0200' ) );
check( 'unlinked extra has no anchor of its own', 1 === substr_count( $html, ' '',
'show_email' => 'yes',
'email_link' => 'yes',
'email_title' => 'Email',
'email_icon' => array( 'value' => 'fas fa-envelope' ),
'show_location' => '',
'show_hours' => '',
'show_license' => '',
'email_extra' => array(
array( 'address' => 'not-an-email', 'label' => 'Bad', 'link' => 'yes' ),
array( 'address' => 'sales@example.com', 'label' => 'Sales', 'link' => 'yes' ),
),
) );
check( 'invalid email is dropped', false === strpos( $html, 'not-an-email' ) );
check( 'valid extra email is linked', false !== strpos( $html, 'href="mailto:sales@example.com"' ) );
// 5. Hours rows carry no inline colon (it comes from CSS ::after).
$html = render_with( array(
'show_phone' => '',
'show_email' => '',
'show_location' => '',
'show_license' => '',
'show_hours' => 'yes',
'hours_title' => 'Hours',
'hours_icon' => array( 'value' => 'far fa-clock' ),
'hours_closed_text' => 'Closed',
) );
check( 'hours row has day and time spans', false !== strpos( $html, 'Monday8:00 AM - 10:00 PM' ) );
check( 'no colon after the day label', false === strpos( $html, 'Monday:' ) );
check( 'a closed day uses the closed text and class', false !== strpos( $html, 'a1tools-hours-closed' ) && false !== strpos( $html, '>Closed' ) );
// 6. Hours CSS contract (3.3.4). These three were shipped in 3.3.3 and then
// explicitly forbidden by the revised spec, so guard the whole block as
// source text — a regression here is invisible in a lint and in markup.
$init = file_get_contents( __DIR__ . '/../includes/a1-tools-elementor-init.php' );
$hours = '';
if ( preg_match( '/Contact Info hours.*?(?=\n\';)/s', $init, $m ) ) {
$hours = $m[0];
}
echo "\nHours CSS contract\n";
check( 'hours CSS block was located', '' !== $hours );
check( 'no colon pseudo-element', false === strpos( $hours, '::after' ) && false === strpos( $hours, 'content: ":"' ) );
check( 'no horizontal scrolling fallback', false === strpos( $hours, 'overflow-x' ) );
check( 'rows wrap', false !== strpos( $hours, 'flex-wrap: wrap' ) );
check( 'nowrap only on the time', 1 === substr_count( $hours, 'white-space: nowrap' ) );
check( 'shrink is container-driven', false !== strpos( $hours, '@container a1hours' ) );
check( 'shrink has a media-query fallback', false !== strpos( $hours, '@supports not (container-type: inline-size)' ) );
check( 'shrink floors at 10px', false === strpos( $hours, 'font-size: 0.' ) && substr_count( $hours, 'max(10px,' ) >= 3 );
// The colon must not come back through the markup either.
$widget = file_get_contents( __DIR__ . '/../includes/class-a1-tools-contact-info-widget.php' );
check( 'no colon in the hours markup or editor template',
false === strpos( $widget, 'a1tools-hours-day">\' . esc_html( $label ) . \':' )
&& false === strpos( $widget, '{{ d.label }}:' ) );
echo $failures ? "\n$failures failure(s)\n" : "\nAll passed\n";
exit( $failures ? 1 : 0 );
}