* feat(ci): adopt estate CI + add PHPUnit suite from scratch (v1.2.0) - Add PHPUnit 9.6 test suite (WP stubs, tests: stubs) — 40 tests across 4 files: MathTest: 25 tests covering all PRC_Math public methods and calculate() orchestrator with real peptide scenarios (BPC-157/TB-500/Semaglutide/MK-677/ Ipamorelin) plus edge cases (zero water, negative inputs, floor rounding, rounding precision, mcg↔mg conversion). DefaultPresetsTest: 11 tests — all 8 expected slugs, required keys per preset (dataProvider), dose_unit validity, min<max range, source field, spot-checks. RestControllerTest: 7 tests — calculate() 200 + correct values (mcg+mg), get_presets() 200, get_preset() 200 for known slug / WP_Error for unknown. PresetProviderTest: 4 tests — fallback to defaults sans PR Core, slug key present, cache invalidation, idempotent invalidation. - Add phpunit.xml.dist + tests/bootstrap.php (WP function/class stubs). - Add composer test script: vendor/bin/phpunit --configuration phpunit.xml.dist. - Add phpunit/phpunit ^9.6 + yoast/phpunit-polyfills ^2.0 to require-dev. - Replace standalone ci.yml with thin caller to estate reusable workflow: uses: peptiderepo/peptide-e2e/.github/workflows/ci.yml@main with: tests: stubs, has_js: true permissions: contents: write (required for phpcbf commit-back) workflow_call (required for deploy.yml gate). - deploy.yml unchanged — already gates on ./ci.yml + uses shared deploy-app.yml. - Version bump 1.1.0 → 1.2.0. Agent-Session: claude/adopt-ci-20260616 * style: auto-fix PHPCS violations via phpcbf * fix(ci): resolve latent PHPCS + JS lint failures Latent debt surfaced by the reusable PHPCS job (previously continue-on-error). PHPCS fixes (all pre-existing, not introduced by this PR): - Add @package PeptideReconstitutionCalculator to file docblocks in all 12 PHP files (previously missing; phpcbf cannot insert missing docblock tags). - Replace two short-ternary ?: operators in class-prc-preset-provider.php with full ternary (Universal.Operators.DisallowShortTernary.Found; lines 136+152). JS lint fix: - Add package.json + package-lock.json (no deps; lint script = node --check) so the reusable lint-js job's npm ci / cache: npm steps succeed. The calculator ships vanilla JS with no build toolchain — package.json is the minimal stub the reusable workflow requires for has_js: true. Agent-Session: claude/adopt-ci-20260616 * style: auto-fix PHPCS violations via phpcbf * fix(phpcs): suppress intentional warnings with phpcs:ignore annotations uninstall.php: direct wpdb->query() intentional in uninstall (no caching available); add phpcs:ignore DirectDatabaseQuery.DirectQuery and NoCaching. class-prc-rest-controller.php get_presets(): request param required by WP REST API callback contract; add phpcs:ignore UnusedFunctionParameter. Agent-Session: claude/adopt-ci-20260616 --------- Co-authored-by: peptiderepo <bot@peptiderepo.com> Co-authored-by: peptiderepo <peptiderepo@users.noreply.github.com>
332 lines
9.2 KiB
PHP
Executable File
332 lines
9.2 KiB
PHP
Executable File
<?php
|
|
/**
|
|
* PHPUnit bootstrap for Peptide Reconstitution Calculator tests.
|
|
*
|
|
* Loads WordPress stubs so unit tests can reference WP functions and classes
|
|
* without requiring a full WordPress installation. All stubs are minimal
|
|
* no-ops sufficient for the tested surface area.
|
|
*
|
|
* @package PRC\Tests
|
|
*/
|
|
|
|
// Composer autoloader.
|
|
require_once dirname( __DIR__ ) . '/vendor/autoload.php';
|
|
|
|
// ── WordPress constant stubs ──────────────────────────────────────────
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
define( 'ABSPATH', '/tmp/wordpress/' );
|
|
}
|
|
if ( ! defined( 'WPINC' ) ) {
|
|
define( 'WPINC', 'wp-includes' );
|
|
}
|
|
|
|
// Plugin constants normally defined in the main bootstrap file.
|
|
if ( ! defined( 'PRC_VERSION' ) ) {
|
|
define( 'PRC_VERSION', '1.2.0-test' );
|
|
}
|
|
if ( ! defined( 'PRC_PLUGIN_FILE' ) ) {
|
|
define( 'PRC_PLUGIN_FILE', dirname( __DIR__ ) . '/peptide-reconstitution-calculator.php' );
|
|
}
|
|
if ( ! defined( 'PRC_PLUGIN_DIR' ) ) {
|
|
define( 'PRC_PLUGIN_DIR', dirname( __DIR__ ) . '/' );
|
|
}
|
|
if ( ! defined( 'PRC_PLUGIN_URL' ) ) {
|
|
define( 'PRC_PLUGIN_URL', 'https://example.com/wp-content/plugins/peptide-reconstitution-calculator/' );
|
|
}
|
|
|
|
// ── WordPress function stubs ──────────────────────────────────────────
|
|
|
|
if ( ! function_exists( 'plugin_dir_path' ) ) {
|
|
function plugin_dir_path( $file ) {
|
|
return trailingslashit( dirname( $file ) );
|
|
}
|
|
}
|
|
if ( ! function_exists( 'plugin_dir_url' ) ) {
|
|
function plugin_dir_url( $file ) {
|
|
return 'https://example.com/wp-content/plugins/' . basename( dirname( $file ) ) . '/';
|
|
}
|
|
}
|
|
if ( ! function_exists( 'plugin_basename' ) ) {
|
|
function plugin_basename( $file ) {
|
|
return basename( dirname( $file ) ) . '/' . basename( $file );
|
|
}
|
|
}
|
|
if ( ! function_exists( 'trailingslashit' ) ) {
|
|
function trailingslashit( $string ) {
|
|
return rtrim( $string, '/\\' ) . '/';
|
|
}
|
|
}
|
|
if ( ! function_exists( 'sanitize_text_field' ) ) {
|
|
function sanitize_text_field( $str ) {
|
|
return trim( strip_tags( $str ) );
|
|
}
|
|
}
|
|
if ( ! function_exists( 'sanitize_title' ) ) {
|
|
function sanitize_title( $title, $fallback_title = '', $context = 'save' ) {
|
|
$title = strip_tags( $title );
|
|
$title = strtolower( $title );
|
|
$title = preg_replace( '/[^a-z0-9\-]/', '-', $title );
|
|
$title = preg_replace( '/-+/', '-', $title );
|
|
return trim( $title, '-' );
|
|
}
|
|
}
|
|
if ( ! function_exists( 'absint' ) ) {
|
|
function absint( $val ) {
|
|
return abs( (int) $val );
|
|
}
|
|
}
|
|
if ( ! function_exists( 'add_action' ) ) {
|
|
function add_action( $tag, $callback, $priority = 10, $accepted_args = 1 ) {
|
|
return true;
|
|
}
|
|
}
|
|
if ( ! function_exists( 'add_filter' ) ) {
|
|
function add_filter( $tag, $callback, $priority = 10, $accepted_args = 1 ) {
|
|
return true;
|
|
}
|
|
}
|
|
if ( ! function_exists( 'add_shortcode' ) ) {
|
|
function add_shortcode( $tag, $callback ) {
|
|
return true;
|
|
}
|
|
}
|
|
if ( ! function_exists( 'shortcode_atts' ) ) {
|
|
function shortcode_atts( $pairs, $atts, $shortcode = '' ) {
|
|
$atts = (array) $atts;
|
|
$out = array();
|
|
foreach ( $pairs as $name => $default ) {
|
|
$out[ $name ] = array_key_exists( $name, $atts ) ? $atts[ $name ] : $default;
|
|
}
|
|
return $out;
|
|
}
|
|
}
|
|
if ( ! function_exists( 'register_rest_route' ) ) {
|
|
function register_rest_route( $namespace, $route, $args = array(), $override = false ) {
|
|
return true;
|
|
}
|
|
}
|
|
if ( ! function_exists( 'rest_url' ) ) {
|
|
function rest_url( $path = '' ) {
|
|
return 'https://example.com/wp-json/' . ltrim( $path, '/' );
|
|
}
|
|
}
|
|
if ( ! function_exists( 'esc_url_raw' ) ) {
|
|
function esc_url_raw( $url, $protocols = null ) {
|
|
return filter_var( $url, FILTER_SANITIZE_URL ) ?: '';
|
|
}
|
|
}
|
|
if ( ! function_exists( 'esc_attr' ) ) {
|
|
function esc_attr( $text ) {
|
|
return htmlspecialchars( $text, ENT_QUOTES, 'UTF-8' );
|
|
}
|
|
}
|
|
if ( ! function_exists( 'esc_html' ) ) {
|
|
function esc_html( $text ) {
|
|
return htmlspecialchars( $text, ENT_QUOTES, 'UTF-8' );
|
|
}
|
|
}
|
|
if ( ! function_exists( 'esc_html__' ) ) {
|
|
function esc_html__( $text, $domain = 'default' ) {
|
|
return htmlspecialchars( $text, ENT_QUOTES, 'UTF-8' );
|
|
}
|
|
}
|
|
if ( ! function_exists( 'esc_html_e' ) ) {
|
|
function esc_html_e( $text, $domain = 'default' ) {
|
|
echo htmlspecialchars( $text, ENT_QUOTES, 'UTF-8' );
|
|
}
|
|
}
|
|
if ( ! function_exists( '__' ) ) {
|
|
function __( $text, $domain = 'default' ) {
|
|
return $text;
|
|
}
|
|
}
|
|
if ( ! function_exists( 'is_admin' ) ) {
|
|
function is_admin() {
|
|
global $_test_is_admin;
|
|
return ! empty( $_test_is_admin );
|
|
}
|
|
}
|
|
if ( ! function_exists( 'wp_enqueue_style' ) ) {
|
|
function wp_enqueue_style( $handle, $src = '', $deps = array(), $ver = false, $media = 'all' ) {
|
|
return true;
|
|
}
|
|
}
|
|
if ( ! function_exists( 'wp_enqueue_script' ) ) {
|
|
function wp_enqueue_script( $handle, $src = '', $deps = array(), $ver = false, $args = array() ) {
|
|
return true;
|
|
}
|
|
}
|
|
if ( ! function_exists( 'wp_localize_script' ) ) {
|
|
function wp_localize_script( $handle, $object_name, $l10n ) {
|
|
return true;
|
|
}
|
|
}
|
|
if ( ! function_exists( 'get_transient' ) ) {
|
|
function get_transient( $transient ) {
|
|
global $_test_transients;
|
|
return isset( $_test_transients[ $transient ] ) ? $_test_transients[ $transient ] : false;
|
|
}
|
|
}
|
|
if ( ! function_exists( 'set_transient' ) ) {
|
|
function set_transient( $transient, $value, $expiration = 0 ) {
|
|
global $_test_transients;
|
|
$_test_transients[ $transient ] = $value;
|
|
return true;
|
|
}
|
|
}
|
|
if ( ! function_exists( 'delete_transient' ) ) {
|
|
function delete_transient( $transient ) {
|
|
global $_test_transients;
|
|
unset( $_test_transients[ $transient ] );
|
|
return true;
|
|
}
|
|
}
|
|
if ( ! function_exists( 'register_activation_hook' ) ) {
|
|
function register_activation_hook( $file, $callback ) {
|
|
return true;
|
|
}
|
|
}
|
|
if ( ! function_exists( 'register_deactivation_hook' ) ) {
|
|
function register_deactivation_hook( $file, $callback ) {
|
|
return true;
|
|
}
|
|
}
|
|
if ( ! function_exists( 'delete_option' ) ) {
|
|
function delete_option( $option ) {
|
|
global $_test_options;
|
|
unset( $_test_options[ $option ] );
|
|
return true;
|
|
}
|
|
}
|
|
if ( ! function_exists( 'wp_cache_flush' ) ) {
|
|
function wp_cache_flush() {
|
|
return true;
|
|
}
|
|
}
|
|
if ( ! function_exists( 'get_option' ) ) {
|
|
function get_option( $option, $default = false ) {
|
|
global $_test_options;
|
|
return isset( $_test_options[ $option ] ) ? $_test_options[ $option ] : $default;
|
|
}
|
|
}
|
|
if ( ! function_exists( 'do_action' ) ) {
|
|
function do_action( $tag, ...$args ) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// WP_REST_Request stub used by REST controller tests.
|
|
if ( ! class_exists( 'WP_REST_Request' ) ) {
|
|
class WP_REST_Request {
|
|
/** @var array<string, mixed> */
|
|
private array $params = array();
|
|
private string $method;
|
|
|
|
public function __construct( string $method = 'GET', string $route = '' ) {
|
|
$this->method = $method;
|
|
}
|
|
|
|
/**
|
|
* @param string $key
|
|
* @param mixed $value
|
|
*/
|
|
public function set_param( string $key, $value ): void {
|
|
$this->params[ $key ] = $value;
|
|
}
|
|
|
|
/**
|
|
* @param string $key
|
|
* @return mixed
|
|
*/
|
|
public function get_param( string $key ) {
|
|
return $this->params[ $key ] ?? null;
|
|
}
|
|
|
|
/** @return array<string, mixed> */
|
|
public function get_params(): array {
|
|
return $this->params;
|
|
}
|
|
}
|
|
}
|
|
|
|
// WP_REST_Response stub.
|
|
if ( ! class_exists( 'WP_REST_Response' ) ) {
|
|
class WP_REST_Response {
|
|
/** @var mixed */
|
|
public $data;
|
|
public int $status;
|
|
|
|
/**
|
|
* @param mixed $data
|
|
*/
|
|
public function __construct( $data = null, int $status = 200 ) {
|
|
$this->data = $data;
|
|
$this->status = $status;
|
|
}
|
|
|
|
/** @return mixed */
|
|
public function get_data() {
|
|
return $this->data;
|
|
}
|
|
|
|
public function get_status(): int {
|
|
return $this->status;
|
|
}
|
|
}
|
|
}
|
|
|
|
// WP_Error stub.
|
|
if ( ! class_exists( 'WP_Error' ) ) {
|
|
class WP_Error {
|
|
/** @var array<string, string[]> */
|
|
public array $errors = array();
|
|
/** @var array<string, mixed> */
|
|
public array $error_data = array();
|
|
|
|
/**
|
|
* @param mixed $data
|
|
*/
|
|
public function __construct( string $code = '', string $message = '', $data = '' ) {
|
|
if ( $code ) {
|
|
$this->errors[ $code ][] = $message;
|
|
if ( $data ) {
|
|
$this->error_data[ $code ] = $data;
|
|
}
|
|
}
|
|
}
|
|
|
|
public function get_error_code(): string {
|
|
$codes = array_keys( $this->errors );
|
|
return $codes ? $codes[0] : '';
|
|
}
|
|
|
|
public function get_error_message( string $code = '' ): string {
|
|
if ( ! $code ) {
|
|
$code = $this->get_error_code();
|
|
}
|
|
return isset( $this->errors[ $code ] ) ? $this->errors[ $code ][0] : '';
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( ! function_exists( 'is_wp_error' ) ) {
|
|
function is_wp_error( $thing ): bool {
|
|
return ( $thing instanceof WP_Error );
|
|
}
|
|
}
|
|
|
|
// ── Load plugin classes for testing ──────────────────────────────────
|
|
|
|
// The autoloader resolves PRC_ classes from includes/ — register it now.
|
|
require_once PRC_PLUGIN_DIR . 'includes/class-prc-autoloader.php';
|
|
PRC_Autoloader::register();
|
|
|
|
// Load pure classes explicitly (no WP hooks fire at require time).
|
|
require_once PRC_PLUGIN_DIR . 'includes/class-prc-math.php';
|
|
require_once PRC_PLUGIN_DIR . 'includes/class-prc-default-presets.php';
|
|
require_once PRC_PLUGIN_DIR . 'includes/class-prc-calculator.php';
|
|
require_once PRC_PLUGIN_DIR . 'includes/class-prc-preset-provider.php';
|
|
require_once PRC_PLUGIN_DIR . 'includes/api/class-prc-rest-controller.php';
|
|
require_once PRC_PLUGIN_DIR . 'includes/frontend/class-prc-shortcode.php';
|