diff --git a/framework/Exceptions/messages/messages.txt b/framework/Exceptions/messages/messages.txt index 734015314..ffa3ed497 100644 --- a/framework/Exceptions/messages/messages.txt +++ b/framework/Exceptions/messages/messages.txt @@ -753,6 +753,9 @@ figcaption_requires_figure_parent = TFigureCaption '{0}' must be a direct child gravatar_bad_default = TGravatar DefaultImageStyle property value '{0}' is not supported gravatar_bad_size = TGravatar Size is not between [1 .. 512] but is '{0}' +progress_value_invalid = TProgress Value must be 0 or greater; {0} given. +progress_max_invalid = TProgress Max must be greater than 0; {0} given. + time_invalid_datetime = TTime DateTime must be a DateTimeInterface, DateInterval, string, or numeric value; {0} given. time_invalid_text_format = TTime TextFormat '{0}' is not a TTimeFormat constant name or ICU datetime pattern. diff --git a/framework/Web/UI/ActiveControls/TActiveMeter.php b/framework/Web/UI/ActiveControls/TActiveMeter.php new file mode 100644 index 000000000..5f8a96ba3 --- /dev/null +++ b/framework/Web/UI/ActiveControls/TActiveMeter.php @@ -0,0 +1,157 @@ + + * @link https://github.com/pradosoft/prado + * @license https://github.com/pradosoft/prado/blob/master/LICENSE + */ + +namespace Prado\Web\UI\ActiveControls; + +use Prado\TPropertyValue; +use Prado\Web\UI\WebControls\TMeter; + +/** + * TActiveMeter class. + * + * TActiveMeter is the active control counterpart to {@see TMeter}. The + * {@see TMeter::setValue Value}, {@see TMeter::setMin Min}, + * {@see TMeter::setMax Max}, {@see TMeter::setLow Low}, + * {@see TMeter::setHigh High}, and {@see TMeter::setOptimum Optimum} + * properties can be changed server-side during a callback and the `` + * element is updated on the client automatically when + * {@see \Prado\Web\UI\ActiveControls\TBaseActiveControl::setEnableUpdate + * ActiveControl.EnableUpdate} is `true` (the default). Setting + * {@see TMeter::setLow Low}, {@see TMeter::setHigh High}, or + * {@see TMeter::setOptimum Optimum} to `null` removes the corresponding + * attribute. + * + * @author Brad Anderson + * @since 4.4.0 + * @method TActiveControlAdapter getAdapter() + */ +class TActiveMeter extends TMeter implements IActiveControl +{ + /** + * Creates a new callback control, sets the adapter to + * TActiveControlAdapter. If you override this class, be sure to set the + * adapter appropriately by, for example, by calling this constructor. + */ + public function __construct() + { + parent::__construct(); + $this->setAdapter(new TActiveControlAdapter($this)); + } + + /** + * @return TBaseActiveControl basic active control options. + */ + public function getActiveControl() + { + return $this->getAdapter()->getBaseActiveControl(); + } + + /** + * Updates an attribute on the client-side when its value changed during a + * callback. A `null` value removes the attribute. + * @param string $name the attribute name + * @param ?float $old the property value before the change + * @param ?float $new the property value after the change + */ + protected function updateClientAttribute(string $name, $old, $new) + { + if ($old === $new || !$this->getActiveControl()->canUpdateClientSide()) { + return; + } + $cs = $this->getPage()->getCallbackClient(); + if ($new === null) { + $cs->removeAttribute($this, $name); + } else { + $cs->setAttribute($this, $name, TPropertyValue::ensureString($new)); + } + } + + /** + * Sets the measured value. On callback response, the `value` attribute is + * updated on the client. + * @param float|string $value the measured value + */ + public function setValue($value) + { + $old = parent::getValue(); + parent::setValue($value); + $this->updateClientAttribute('value', $old, $this->getValue()); + } + + /** + * Sets the lower bound of the range. On callback response, the `min` + * attribute is updated on the client. + * @param float|string $value the lower bound of the range + */ + public function setMin($value) + { + $old = parent::getMin(); + parent::setMin($value); + $this->updateClientAttribute('min', $old, $this->getMin()); + } + + /** + * Sets the upper bound of the range. On callback response, the `max` + * attribute is updated on the client. + * @param float|string $value the upper bound of the range + */ + public function setMax($value) + { + $old = parent::getMax(); + parent::setMax($value); + $this->updateClientAttribute('max', $old, $this->getMax()); + } + + /** + * Sets the upper bound of the "low" segment. On callback response, the + * `low` attribute is updated on the client; null removes it. + * @param null|float|string $value the upper bound of the "low" segment + */ + public function setLow($value) + { + $old = parent::getLow(); + parent::setLow($value); + $this->updateClientAttribute('low', $old, $this->getLow()); + } + + /** + * Sets the lower bound of the "high" segment. On callback response, the + * `high` attribute is updated on the client; null removes it. + * @param null|float|string $value the lower bound of the "high" segment + */ + public function setHigh($value) + { + $old = parent::getHigh(); + parent::setHigh($value); + $this->updateClientAttribute('high', $old, $this->getHigh()); + } + + /** + * Sets the optimum point in the range. On callback response, the `optimum` + * attribute is updated on the client; null removes it. + * @param null|float|string $value the optimum point in the range + */ + public function setOptimum($value) + { + $old = parent::getOptimum(); + parent::setOptimum($value); + $this->updateClientAttribute('optimum', $old, $this->getOptimum()); + } + + /** + * Adds attribute id to the renderer so the client can locate the element. + * @param \Prado\Web\UI\THtmlWriter $writer the writer used for the rendering purpose + */ + protected function addAttributesToRender($writer) + { + $writer->addAttribute('id', $this->getClientID()); + parent::addAttributesToRender($writer); + } +} diff --git a/framework/Web/UI/ActiveControls/TActiveProgress.php b/framework/Web/UI/ActiveControls/TActiveProgress.php new file mode 100644 index 000000000..e75968dae --- /dev/null +++ b/framework/Web/UI/ActiveControls/TActiveProgress.php @@ -0,0 +1,107 @@ + + * @link https://github.com/pradosoft/prado + * @license https://github.com/pradosoft/prado/blob/master/LICENSE + */ + +namespace Prado\Web\UI\ActiveControls; + +use Prado\TPropertyValue; +use Prado\Web\UI\WebControls\TProgress; + +/** + * TActiveProgress class. + * + * TActiveProgress is the active control counterpart to {@see TProgress}. The + * {@see TProgress::setValue Value} and {@see TProgress::setMax Max} properties + * can be changed server-side during a callback and the `` element is + * updated on the client automatically when + * {@see \Prado\Web\UI\ActiveControls\TBaseActiveControl::setEnableUpdate + * ActiveControl.EnableUpdate} is `true` (the default). Setting + * {@see TProgress::setValue Value} to `null` removes the `value` attribute, + * returning the bar to its indeterminate state. + * + * A common pattern pairs TActiveProgress with a + * {@see \Prado\Web\UI\ActiveControls\TTimeTriggeredCallback} polling a + * server-side task for completion updates. + * + * @author Brad Anderson + * @since 4.4.0 + * @method TActiveControlAdapter getAdapter() + */ +class TActiveProgress extends TProgress implements IActiveControl +{ + /** + * Creates a new callback control, sets the adapter to + * TActiveControlAdapter. If you override this class, be sure to set the + * adapter appropriately by, for example, by calling this constructor. + */ + public function __construct() + { + parent::__construct(); + $this->setAdapter(new TActiveControlAdapter($this)); + } + + /** + * @return TBaseActiveControl basic active control options. + */ + public function getActiveControl() + { + return $this->getAdapter()->getBaseActiveControl(); + } + + /** + * Sets how much of the task has completed. On callback response, the + * `value` attribute is updated on the client; a `null` value removes the + * attribute for an indeterminate bar. + * @param null|float|string $value the completed amount, 0 or greater; + * null or empty string displays an indeterminate progress bar + */ + public function setValue($value) + { + $old = parent::getValue(); + parent::setValue($value); + if ($old === $this->getValue()) { + return; + } + if ($this->getActiveControl()->canUpdateClientSide()) { + $cs = $this->getPage()->getCallbackClient(); + if (($value = $this->getValue()) === null) { + $cs->removeAttribute($this, 'value'); + } else { + $cs->setAttribute($this, 'value', TPropertyValue::ensureString($value)); + } + } + } + + /** + * Sets how much work the task requires in total. On callback response, the + * `max` attribute is updated on the client. + * @param float|string $value the total amount of work, greater than 0 + */ + public function setMax($value) + { + $old = parent::getMax(); + parent::setMax($value); + if ($old === $this->getMax()) { + return; + } + if ($this->getActiveControl()->canUpdateClientSide()) { + $this->getPage()->getCallbackClient()->setAttribute($this, 'max', TPropertyValue::ensureString($this->getMax())); + } + } + + /** + * Adds attribute id to the renderer so the client can locate the element. + * @param \Prado\Web\UI\THtmlWriter $writer the writer used for the rendering purpose + */ + protected function addAttributesToRender($writer) + { + $writer->addAttribute('id', $this->getClientID()); + parent::addAttributesToRender($writer); + } +} diff --git a/framework/Web/UI/WebControls/TMeter.php b/framework/Web/UI/WebControls/TMeter.php new file mode 100644 index 000000000..7030fbd0d --- /dev/null +++ b/framework/Web/UI/WebControls/TMeter.php @@ -0,0 +1,195 @@ + + * @link https://github.com/pradosoft/prado + * @license https://github.com/pradosoft/prado/blob/master/LICENSE + */ + +namespace Prado\Web\UI\WebControls; + +use Prado\TPropertyValue; + +/** + * TMeter class + * + * TMeter represents the HTML5 `` element. The `` element + * represents a scalar measurement within a known range, such as disk usage, + * a relevance score, or a fraction of a voting population. + * + * Properties: + * - Value, float — the measured value, rendered as the `value` attribute. + * Defaults to `0.0`. The attribute is always rendered; the HTML specification + * requires it. + * - Min, float — the lower bound of the range, rendered as the `min` + * attribute when not `0.0`. Defaults to `0.0`. + * - Max, float — the upper bound of the range, rendered as the `max` + * attribute when not `1.0`. Defaults to `1.0`. + * - Low, ?float — the upper bound of the "low" segment of the range, + * rendered as the `low` attribute. `null` (the default) renders no attribute. + * - High, ?float — the lower bound of the "high" segment of the range, + * rendered as the `high` attribute. `null` (the default) renders no attribute. + * - Optimum, ?float — the optimum point in the range, rendered as the + * `optimum` attribute. `null` (the default) renders no attribute. + * + * The browser clamps out-of-range combinations per the HTML specification: + * `value`, `low`, `high`, and `optimum` are constrained into `[min, max]`. + * The properties accept any float so they can be assigned in any order. + * + * Child content renders inside the element as fallback for user agents without + * `` support. Associate a caption with {@see TLabel} and its + * {@see TLabel::setForControl ForControl} property. + * + * The gauge's colors are styled with CSS pseudo-elements + * (`::-webkit-meter-bar`, `::-webkit-meter-optimum-value`, + * `::-webkit-meter-suboptimum-value`, `::-moz-meter-bar`); use + * {@see setCssClass CssClass} with a stylesheet for theming. + * + * Template usage: + * ```html + * + * + * ``` + * + * @author Brad Anderson + * @since 4.4.0 + */ +class TMeter extends TWebControl +{ + /** + * @return string tag name + */ + protected function getTagName() + { + return 'meter'; + } + + /** + * @return float the measured value. Defaults to 0.0. + */ + public function getValue() + { + return $this->getViewState('Value', 0.0); + } + + /** + * @param float|string $value the measured value + */ + public function setValue($value) + { + $this->setViewState('Value', TPropertyValue::ensureFloat($value), 0.0); + } + + /** + * @return float the lower bound of the range. Defaults to 0.0. + */ + public function getMin() + { + return $this->getViewState('Min', 0.0); + } + + /** + * @param float|string $value the lower bound of the range + */ + public function setMin($value) + { + $this->setViewState('Min', TPropertyValue::ensureFloat($value), 0.0); + } + + /** + * @return float the upper bound of the range. Defaults to 1.0. + */ + public function getMax() + { + return $this->getViewState('Max', 1.0); + } + + /** + * @param float|string $value the upper bound of the range + */ + public function setMax($value) + { + $this->setViewState('Max', TPropertyValue::ensureFloat($value), 1.0); + } + + /** + * @return ?float the upper bound of the "low" segment of the range; + * null (the default) renders no attribute + */ + public function getLow() + { + return $this->getViewState('Low', null); + } + + /** + * @param null|float|string $value the upper bound of the "low" segment; + * null or empty string renders no attribute + */ + public function setLow($value) + { + $this->setViewState('Low', ($value === null || $value === '') ? null : TPropertyValue::ensureFloat($value), null); + } + + /** + * @return ?float the lower bound of the "high" segment of the range; + * null (the default) renders no attribute + */ + public function getHigh() + { + return $this->getViewState('High', null); + } + + /** + * @param null|float|string $value the lower bound of the "high" segment; + * null or empty string renders no attribute + */ + public function setHigh($value) + { + $this->setViewState('High', ($value === null || $value === '') ? null : TPropertyValue::ensureFloat($value), null); + } + + /** + * @return ?float the optimum point in the range; null (the default) renders + * no attribute + */ + public function getOptimum() + { + return $this->getViewState('Optimum', null); + } + + /** + * @param null|float|string $value the optimum point in the range; + * null or empty string renders no attribute + */ + public function setOptimum($value) + { + $this->setViewState('Optimum', ($value === null || $value === '') ? null : TPropertyValue::ensureFloat($value), null); + } + + /** + * Adds attribute name-value pairs to renderer. + * @param \Prado\Web\UI\THtmlWriter $writer the writer used for the rendering purpose + */ + protected function addAttributesToRender($writer) + { + parent::addAttributesToRender($writer); + if (($min = $this->getMin()) !== 0.0) { + $writer->addAttribute('min', TPropertyValue::ensureString($min)); + } + if (($max = $this->getMax()) !== 1.0) { + $writer->addAttribute('max', TPropertyValue::ensureString($max)); + } + if (($low = $this->getLow()) !== null) { + $writer->addAttribute('low', TPropertyValue::ensureString($low)); + } + if (($high = $this->getHigh()) !== null) { + $writer->addAttribute('high', TPropertyValue::ensureString($high)); + } + if (($optimum = $this->getOptimum()) !== null) { + $writer->addAttribute('optimum', TPropertyValue::ensureString($optimum)); + } + $writer->addAttribute('value', TPropertyValue::ensureString($this->getValue())); + } +} diff --git a/framework/Web/UI/WebControls/TProgress.php b/framework/Web/UI/WebControls/TProgress.php new file mode 100644 index 000000000..33d35af6b --- /dev/null +++ b/framework/Web/UI/WebControls/TProgress.php @@ -0,0 +1,121 @@ + + * @link https://github.com/pradosoft/prado + * @license https://github.com/pradosoft/prado/blob/master/LICENSE + */ + +namespace Prado\Web\UI\WebControls; + +use Prado\Exceptions\TInvalidDataValueException; +use Prado\TPropertyValue; + +/** + * TProgress class + * + * TProgress represents the HTML5 `` element. The `` element + * represents the completion progress of a task. + * + * Properties: + * - Value, ?float — how much of the task has completed, rendered as the + * `value` attribute. Must be 0 or greater. `null` (the default) renders no + * `value` attribute, which displays the browser's indeterminate progress bar. + * - Max, float — how much work the task requires in total, rendered as + * the `max` attribute when not `1.0`. Must be greater than 0. Defaults to `1.0`. + * + * Child content renders inside the element as fallback for user agents without + * `` support. Associate a caption with {@see TLabel} and its + * {@see TLabel::setForControl ForControl} property. + * + * The bar's colors are styled with CSS pseudo-elements (`::-webkit-progress-bar`, + * `::-webkit-progress-value`, `::-moz-progress-bar`) or the `accent-color` + * property; use {@see setCssClass CssClass} with a stylesheet for theming. + * + * Template usage: + * ```html + * + * + * + * ``` + * + * @author Brad Anderson + * @since 4.4.0 + */ +class TProgress extends TWebControl +{ + /** + * @return string tag name + */ + protected function getTagName() + { + return 'progress'; + } + + /** + * @return ?float how much of the task has completed; null (the default) + * displays an indeterminate progress bar + */ + public function getValue() + { + return $this->getViewState('Value', null); + } + + /** + * Sets how much of the task has completed. + * @param null|float|string $value the completed amount, 0 or greater; + * null or empty string displays an indeterminate progress bar + * @throws TInvalidDataValueException if the value is less than 0 + */ + public function setValue($value) + { + if ($value === null || $value === '') { + $this->setViewState('Value', null, null); + return; + } + $value = TPropertyValue::ensureFloat($value); + if ($value < 0) { + throw new TInvalidDataValueException('progress_value_invalid', $value); + } + $this->setViewState('Value', $value, null); + } + + /** + * @return float how much work the task requires in total. Defaults to 1.0. + */ + public function getMax() + { + return $this->getViewState('Max', 1.0); + } + + /** + * Sets how much work the task requires in total. + * @param float|string $value the total amount of work, greater than 0 + * @throws TInvalidDataValueException if the value is not greater than 0 + */ + public function setMax($value) + { + $value = TPropertyValue::ensureFloat($value); + if ($value <= 0) { + throw new TInvalidDataValueException('progress_max_invalid', $value); + } + $this->setViewState('Max', $value, 1.0); + } + + /** + * Adds attribute name-value pairs to renderer. + * @param \Prado\Web\UI\THtmlWriter $writer the writer used for the rendering purpose + */ + protected function addAttributesToRender($writer) + { + parent::addAttributesToRender($writer); + if (($value = $this->getValue()) !== null) { + $writer->addAttribute('value', TPropertyValue::ensureString($value)); + } + if (($max = $this->getMax()) !== 1.0) { + $writer->addAttribute('max', TPropertyValue::ensureString($max)); + } + } +} diff --git a/framework/classes.php b/framework/classes.php index a4958441b..eb655b442 100644 --- a/framework/classes.php +++ b/framework/classes.php @@ -551,10 +551,12 @@ 'TActiveListControlAdapter' => 'Prado\Web\UI\ActiveControls\TActiveListControlAdapter', 'TActiveListItemCollection' => 'Prado\Web\UI\ActiveControls\TActiveListItemCollection', 'TActiveLiteralColumn' => 'Prado\Web\UI\ActiveControls\TActiveLiteralColumn', +'TActiveMeter' => 'Prado\Web\UI\ActiveControls\TActiveMeter', 'TActiveMultiView' => 'Prado\Web\UI\ActiveControls\TActiveMultiView', 'TActivePageAdapter' => 'Prado\Web\UI\ActiveControls\TActivePageAdapter', 'TActivePager' => 'Prado\Web\UI\ActiveControls\TActivePager', 'TActivePanel' => 'Prado\Web\UI\ActiveControls\TActivePanel', +'TActiveProgress' => 'Prado\Web\UI\ActiveControls\TActiveProgress', 'TActiveRadioButton' => 'Prado\Web\UI\ActiveControls\TActiveRadioButton', 'TActiveRadioButtonItem' => 'Prado\Web\UI\ActiveControls\TActiveRadioButtonItem', 'TActiveRadioButtonList' => 'Prado\Web\UI\ActiveControls\TActiveRadioButtonList', @@ -788,6 +790,7 @@ 'TMarkdown' => 'Prado\Web\UI\WebControls\TMarkdown', 'TMetaTag' => 'Prado\Web\UI\WebControls\TMetaTag', 'TMetaTagCollection' => 'Prado\Web\UI\WebControls\TMetaTagCollection', +'TMeter' => 'Prado\Web\UI\WebControls\TMeter', 'TMultiView' => 'Prado\Web\UI\WebControls\TMultiView', 'TNav' => 'Prado\Web\UI\WebControls\TNav', 'TOutputCache' => 'Prado\Web\UI\WebControls\TOutputCache', @@ -803,6 +806,7 @@ 'TPanelStyle' => 'Prado\Web\UI\WebControls\TPanelStyle', 'TPlaceHolder' => 'Prado\Web\UI\WebControls\TPlaceHolder', 'TPolygonHotSpot' => 'Prado\Web\UI\WebControls\TPolygonHotSpot', +'TProgress' => 'Prado\Web\UI\WebControls\TProgress', 'TRadioButton' => 'Prado\Web\UI\WebControls\TRadioButton', 'TRadioButtonItem' => 'Prado\Web\UI\WebControls\TRadioButtonItem', 'TRadioButtonList' => 'Prado\Web\UI\WebControls\TRadioButtonList', diff --git a/tests/harness/active-controls/protected/pages/ActiveMeterTest.page b/tests/harness/active-controls/protected/pages/ActiveMeterTest.page new file mode 100644 index 000000000..b7328422f --- /dev/null +++ b/tests/harness/active-controls/protected/pages/ActiveMeterTest.page @@ -0,0 +1,9 @@ + +

Active Meter Test Case

+ + + + + + +
diff --git a/tests/harness/active-controls/protected/pages/ActiveMeterTest.php b/tests/harness/active-controls/protected/pages/ActiveMeterTest.php new file mode 100644 index 000000000..148870869 --- /dev/null +++ b/tests/harness/active-controls/protected/pages/ActiveMeterTest.php @@ -0,0 +1,26 @@ +meter1->Max = 100; + $this->meter1->Value = 70; + $this->meter1->Low = 25; + $this->meter1->High = 85; + $this->meter1->Optimum = 10; + } + + public function clear_segments() + { + $this->meter1->Low = null; + $this->meter1->High = null; + $this->meter1->Optimum = null; + } + + public function shift_range() + { + $this->meter1->Min = -10; + $this->meter1->Value = -5; + } +} diff --git a/tests/harness/active-controls/protected/pages/ActiveProgressTest.page b/tests/harness/active-controls/protected/pages/ActiveProgressTest.page new file mode 100644 index 000000000..d2f76fcad --- /dev/null +++ b/tests/harness/active-controls/protected/pages/ActiveProgressTest.page @@ -0,0 +1,11 @@ + +

Active Progress Test Case

+ + + + + + + + +
diff --git a/tests/harness/active-controls/protected/pages/ActiveProgressTest.php b/tests/harness/active-controls/protected/pages/ActiveProgressTest.php new file mode 100644 index 000000000..7d4cbc88a --- /dev/null +++ b/tests/harness/active-controls/protected/pages/ActiveProgressTest.php @@ -0,0 +1,25 @@ +progress1->Value = 0.5; + } + + public function set_scale() + { + $this->progress1->Max = 100; + $this->progress1->Value = 75; + } + + public function set_indeterminate() + { + $this->progress1->Value = null; + } + + public function start_second() + { + $this->progress2->Value = 0.4; + } +} diff --git a/tests/playwright/active-controls/ActiveMeterTestCase.spec.js b/tests/playwright/active-controls/ActiveMeterTestCase.spec.js new file mode 100644 index 000000000..17e5e5d4d --- /dev/null +++ b/tests/playwright/active-controls/ActiveMeterTestCase.spec.js @@ -0,0 +1,36 @@ +import { test, expect } from '@playwright/test'; +import { genericHelper } from '../helpers.js'; + +test('ActiveMeterTestCase', async ({ page }) => { + const h = genericHelper(page); + const base = 'ctl0_Content_'; + await h.url('active-controls/index.php?page=ActiveMeterTest'); + await h.assertSourceContains('Active Meter Test Case'); + + // Initial render: value only, defaults omitted + await h.assertAttribute(`${base}meter1@value`, '0.25'); + await h.assertAttribute(`${base}meter1@low`, null); + + // Server sets scale and segment boundaries during a callback + await h.click(`${base}button1`); + await h.waitForAjaxCalls(); + await h.assertAttribute(`${base}meter1@max`, '100'); + await h.assertAttribute(`${base}meter1@value`, '70'); + await h.assertAttribute(`${base}meter1@low`, '25'); + await h.assertAttribute(`${base}meter1@high`, '85'); + await h.assertAttribute(`${base}meter1@optimum`, '10'); + + // Server sets segments to null: the attributes are removed + await h.click(`${base}button2`); + await h.waitForAjaxCalls(); + await h.assertAttribute(`${base}meter1@low`, null); + await h.assertAttribute(`${base}meter1@high`, null); + await h.assertAttribute(`${base}meter1@optimum`, null); + await h.assertAttribute(`${base}meter1@value`, '70'); + + // Server shifts the range into negative values + await h.click(`${base}button3`); + await h.waitForAjaxCalls(); + await h.assertAttribute(`${base}meter1@min`, '-10'); + await h.assertAttribute(`${base}meter1@value`, '-5'); +}); diff --git a/tests/playwright/active-controls/ActiveProgressTestCase.spec.js b/tests/playwright/active-controls/ActiveProgressTestCase.spec.js new file mode 100644 index 000000000..03e469c57 --- /dev/null +++ b/tests/playwright/active-controls/ActiveProgressTestCase.spec.js @@ -0,0 +1,34 @@ +import { test, expect } from '@playwright/test'; +import { genericHelper } from '../helpers.js'; + +test('ActiveProgressTestCase', async ({ page }) => { + const h = genericHelper(page); + const base = 'ctl0_Content_'; + await h.url('active-controls/index.php?page=ActiveProgressTest'); + await h.assertSourceContains('Active Progress Test Case'); + + // Initial render: progress1 determinate at 0.25, progress2 indeterminate + await h.assertAttribute(`${base}progress1@value`, '0.25'); + await h.assertAttribute(`${base}progress2@value`, null); + + // Server updates the value during a callback + await h.click(`${base}button1`); + await h.waitForAjaxCalls(); + await h.assertAttribute(`${base}progress1@value`, '0.5'); + + // Server updates max and value together + await h.click(`${base}button2`); + await h.waitForAjaxCalls(); + await h.assertAttribute(`${base}progress1@max`, '100'); + await h.assertAttribute(`${base}progress1@value`, '75'); + + // Server sets Value to null: the value attribute is removed (indeterminate) + await h.click(`${base}button3`); + await h.waitForAjaxCalls(); + await h.assertAttribute(`${base}progress1@value`, null); + + // Server gives the indeterminate bar a value: the attribute is added + await h.click(`${base}button4`); + await h.waitForAjaxCalls(); + await h.assertAttribute(`${base}progress2@value`, '0.4'); +}); diff --git a/tests/unit/Web/UI/ActiveControls/TActiveMeterTest.php b/tests/unit/Web/UI/ActiveControls/TActiveMeterTest.php new file mode 100644 index 000000000..9d7e756d2 --- /dev/null +++ b/tests/unit/Web/UI/ActiveControls/TActiveMeterTest.php @@ -0,0 +1,86 @@ +assertInstanceOf(TMeter::class, $control); + } + + public function testImplementsIActiveControl() + { + $control = new TActiveMeter(); + $this->assertInstanceOf(IActiveControl::class, $control); + } + + public function testGetActiveControlReturnsBaseActiveControl() + { + $control = new TActiveMeter(); + $this->assertInstanceOf( + \Prado\Web\UI\ActiveControls\TBaseActiveControl::class, + $control->getActiveControl() + ); + } + + // --- setters without active page context (no canUpdateClientSide) --- + + public function testSettersWithoutPage() + { + $control = new TActiveMeter(); + $control->setValue(70); + $control->setMin(-10); + $control->setMax(100); + $control->setLow(25); + $control->setHigh(85); + $control->setOptimum(10); + + $this->assertSame(70.0, $control->getValue()); + $this->assertSame(-10.0, $control->getMin()); + $this->assertSame(100.0, $control->getMax()); + $this->assertSame(25.0, $control->getLow()); + $this->assertSame(85.0, $control->getHigh()); + $this->assertSame(10.0, $control->getOptimum()); + } + + public function testSetLowNullWithoutPage() + { + $control = new TActiveMeter(); + $control->setLow(25); + $control->setLow(null); + $this->assertNull($control->getLow()); + } + + public function testSetValueSameValueNoOp() + { + $control = new TActiveMeter(); + $control->setValue(0.6); + // Setting the same value again short-circuits before any client update + $control->setValue('0.6'); + $this->assertSame(0.6, $control->getValue()); + } + + public function testSetLowNullTwiceNoOp() + { + $control = new TActiveMeter(); + // null -> null takes the no-change path before any client update + $control->setLow(null); + $this->assertNull($control->getLow()); + } + + public function testSetMinSameValueNoOp() + { + $control = new TActiveMeter(); + $control->setMin(-10); + $control->setMin('-10'); + $this->assertSame(-10.0, $control->getMin()); + } + + // Note: rendering TActiveMeter requires a full page context; client-side + // attribute updates are covered by the ActiveMeterTestCase functional test. +} diff --git a/tests/unit/Web/UI/ActiveControls/TActiveProgressTest.php b/tests/unit/Web/UI/ActiveControls/TActiveProgressTest.php new file mode 100644 index 000000000..835913481 --- /dev/null +++ b/tests/unit/Web/UI/ActiveControls/TActiveProgressTest.php @@ -0,0 +1,98 @@ +assertInstanceOf(TProgress::class, $control); + } + + public function testImplementsIActiveControl() + { + $control = new TActiveProgress(); + $this->assertInstanceOf(IActiveControl::class, $control); + } + + public function testGetActiveControlReturnsBaseActiveControl() + { + $control = new TActiveProgress(); + $this->assertInstanceOf( + \Prado\Web\UI\ActiveControls\TBaseActiveControl::class, + $control->getActiveControl() + ); + } + + // --- setValue / setMax without active page context (no canUpdateClientSide) --- + + public function testSetValueWithoutPage() + { + $control = new TActiveProgress(); + // canUpdateClientSide() returns false without a page, so no client update attempted + $control->setValue(0.5); + $this->assertSame(0.5, $control->getValue()); + } + + public function testSetValueNullWithoutPage() + { + $control = new TActiveProgress(); + $control->setValue(0.5); + $control->setValue(null); + $this->assertNull($control->getValue()); + } + + public function testSetValueSameValueNoOp() + { + $control = new TActiveProgress(); + $control->setValue(0.5); + // Setting the same value again short-circuits before any client update + $control->setValue('0.5'); + $this->assertSame(0.5, $control->getValue()); + } + + public function testSetValueNullTwiceNoOp() + { + $control = new TActiveProgress(); + // null -> null takes the no-change path before any client update + $control->setValue(null); + $this->assertNull($control->getValue()); + } + + public function testSetMaxSameValueNoOp() + { + $control = new TActiveProgress(); + $control->setMax(100); + // Setting the same max again short-circuits before any client update + $control->setMax('100'); + $this->assertSame(100.0, $control->getMax()); + } + + public function testSetMaxWithoutPage() + { + $control = new TActiveProgress(); + $control->setMax(100); + $this->assertSame(100.0, $control->getMax()); + } + + public function testSetValueNegativeThrows() + { + $control = new TActiveProgress(); + $this->expectException(\Prado\Exceptions\TInvalidDataValueException::class); + $control->setValue(-1); + } + + public function testSetMaxZeroThrows() + { + $control = new TActiveProgress(); + $this->expectException(\Prado\Exceptions\TInvalidDataValueException::class); + $control->setMax(0); + } + + // Note: rendering TActiveProgress requires a full page context; client-side + // attribute updates are covered by the ActiveProgressTestCase functional test. +} diff --git a/tests/unit/Web/UI/WebControls/TMeterTest.php b/tests/unit/Web/UI/WebControls/TMeterTest.php new file mode 100644 index 000000000..89123b3d5 --- /dev/null +++ b/tests/unit/Web/UI/WebControls/TMeterTest.php @@ -0,0 +1,206 @@ +render($control); + $this->assertStringContainsString('assertStringContainsString('
', $output); + } + + public function testExtendsWebControl() + { + $control = new TMeter(); + $this->assertInstanceOf(\Prado\Web\UI\WebControls\TWebControl::class, $control); + } + + // --- Defaults --- + + public function testDefaults() + { + $control = new TMeter(); + $this->assertSame(0.0, $control->getValue()); + $this->assertSame(0.0, $control->getMin()); + $this->assertSame(1.0, $control->getMax()); + $this->assertNull($control->getLow()); + $this->assertNull($control->getHigh()); + $this->assertNull($control->getOptimum()); + } + + // --- Setters --- + + public function testSetValue() + { + $control = new TMeter(); + $control->setValue(0.6); + $this->assertSame(0.6, $control->getValue()); + $control->setValue('70'); + $this->assertSame(70.0, $control->getValue()); + } + + public function testSetMinNegativeAllowed() + { + $control = new TMeter(); + $control->setMin(-10); + $this->assertSame(-10.0, $control->getMin()); + } + + public function testSetMax() + { + $control = new TMeter(); + $control->setMax(100); + $this->assertSame(100.0, $control->getMax()); + } + + public function testSetLowHighOptimum() + { + $control = new TMeter(); + $control->setLow(25); + $control->setHigh(85); + $control->setOptimum(10); + $this->assertSame(25.0, $control->getLow()); + $this->assertSame(85.0, $control->getHigh()); + $this->assertSame(10.0, $control->getOptimum()); + } + + public function testSetLowHighOptimumNullClears() + { + $control = new TMeter(); + $control->setLow(25); + $control->setLow(null); + $this->assertNull($control->getLow()); + + $control->setHigh(85); + $control->setHigh(''); + $this->assertNull($control->getHigh()); + + $control->setOptimum(10); + $control->setOptimum(null); + $this->assertNull($control->getOptimum()); + } + + // --- Rendering --- + + public function testValueAttributeAlwaysRendered() + { + $control = new TMeter(); + $output = $this->render($control); + $this->assertStringContainsString('value="0"', $output); + } + + public function testMinMaxAttributesNotRenderedAtDefaults() + { + $control = new TMeter(); + $output = $this->render($control); + $this->assertStringNotContainsString('min=', $output); + $this->assertStringNotContainsString('max=', $output); + } + + public function testAllAttributesRendered() + { + $control = new TMeter(); + $control->setValue(70); + $control->setMin(-10); + $control->setMax(100); + $control->setLow(25); + $control->setHigh(85); + $control->setOptimum(10); + $output = $this->render($control); + $this->assertStringContainsString('value="70"', $output); + $this->assertStringContainsString('min="-10"', $output); + $this->assertStringContainsString('max="100"', $output); + $this->assertStringContainsString('low="25"', $output); + $this->assertStringContainsString('high="85"', $output); + $this->assertStringContainsString('optimum="10"', $output); + } + + public function testLowHighOptimumNotRenderedWhenNull() + { + $control = new TMeter(); + $output = $this->render($control); + $this->assertStringNotContainsString('low=', $output); + $this->assertStringNotContainsString('high=', $output); + $this->assertStringNotContainsString('optimum=', $output); + } + + public function testScaleAttributesRenderedBeforeValue() + { + // Cosmetic serialization order: min, max, low, high, optimum, then value + $control = new TMeter(); + $control->setValue(70); + $control->setMin(-10); + $control->setMax(100); + $control->setLow(25); + $output = $this->render($control); + $valuePos = strpos($output, 'value='); + $this->assertLessThan($valuePos, strpos($output, 'min=')); + $this->assertLessThan($valuePos, strpos($output, 'max=')); + $this->assertLessThan($valuePos, strpos($output, 'low=')); + } + + public function testNegativeValueRendered() + { + $control = new TMeter(); + $control->setMin(-10); + $control->setValue(-5); + $output = $this->render($control); + $this->assertStringContainsString('value="-5"', $output); + } + + public function testDecimalPrecisionRendered() + { + $control = new TMeter(); + $control->setValue('0.33'); + $output = $this->render($control); + $this->assertStringContainsString('value="0.33"', $output); + } + + public function testMinMaxResetToDefaultsOmitAttributes() + { + $control = new TMeter(); + $control->setMin(-10); + $control->setMax(100); + $control->setMin(0.0); + $control->setMax(1.0); + $output = $this->render($control); + $this->assertStringNotContainsString('min=', $output); + $this->assertStringNotContainsString('max=', $output); + } + + public function testNotVisibleRendersNothing() + { + // Visibility is enforced by renderControl(), the framework's render entry point + $control = new TMeter(); + $control->setValue(0.5); + $control->setVisible(false); + $textWriter = new \Prado\IO\TTextWriter(); + $control->renderControl(new \Prado\Web\UI\THtmlWriter($textWriter)); + $this->assertSame('', $textWriter->flush()); + } + + public function testCssClassAndAttributesRendered() + { + $control = new TMeter(); + $control->setCssClass('disk-gauge'); + $control->getAttributes()->add('data-drive', 'sda1'); + $output = $this->render($control); + $this->assertStringContainsString('class="disk-gauge"', $output); + $this->assertStringContainsString('data-drive="sda1"', $output); + } + + public function testFallbackChildContentRendered() + { + $control = new TMeter(); + $control->setValue(0.6); + $control->getControls()->add('60%'); + $output = $this->render($control); + $this->assertStringContainsString('>60%
', $output); + } +} diff --git a/tests/unit/Web/UI/WebControls/TProgressTest.php b/tests/unit/Web/UI/WebControls/TProgressTest.php new file mode 100644 index 000000000..55afd8158 --- /dev/null +++ b/tests/unit/Web/UI/WebControls/TProgressTest.php @@ -0,0 +1,198 @@ +render($control); + $this->assertStringContainsString('assertStringContainsString('
', $output); + } + + public function testExtendsWebControl() + { + $control = new TProgress(); + $this->assertInstanceOf(\Prado\Web\UI\WebControls\TWebControl::class, $control); + } + + // --- Value --- + + public function testValueDefaultNull() + { + $control = new TProgress(); + $this->assertNull($control->getValue()); + } + + public function testSetValue() + { + $control = new TProgress(); + $control->setValue(0.7); + $this->assertSame(0.7, $control->getValue()); + $control->setValue('35'); + $this->assertSame(35.0, $control->getValue()); + } + + public function testSetValueZeroAllowed() + { + $control = new TProgress(); + $control->setValue(0); + $this->assertSame(0.0, $control->getValue()); + } + + public function testSetValueNullResetsToIndeterminate() + { + $control = new TProgress(); + $control->setValue(0.5); + $control->setValue(null); + $this->assertNull($control->getValue()); + } + + public function testSetValueEmptyStringResetsToIndeterminate() + { + $control = new TProgress(); + $control->setValue(0.5); + $control->setValue(''); + $this->assertNull($control->getValue()); + } + + public function testSetValueNegativeThrows() + { + $control = new TProgress(); + $this->expectException(\Prado\Exceptions\TInvalidDataValueException::class); + $control->setValue(-0.1); + } + + // --- Max --- + + public function testMaxDefaultOne() + { + $control = new TProgress(); + $this->assertSame(1.0, $control->getMax()); + } + + public function testSetMax() + { + $control = new TProgress(); + $control->setMax(100); + $this->assertSame(100.0, $control->getMax()); + } + + public function testSetMaxZeroThrows() + { + $control = new TProgress(); + $this->expectException(\Prado\Exceptions\TInvalidDataValueException::class); + $control->setMax(0); + } + + public function testSetMaxNegativeThrows() + { + $control = new TProgress(); + $this->expectException(\Prado\Exceptions\TInvalidDataValueException::class); + $control->setMax(-1); + } + + // --- Rendering --- + + public function testValueAttributeRendered() + { + $control = new TProgress(); + $control->setValue(0.7); + $output = $this->render($control); + $this->assertStringContainsString('value="0.7"', $output); + } + + public function testValueAttributeNotRenderedWhenIndeterminate() + { + $control = new TProgress(); + $output = $this->render($control); + $this->assertStringNotContainsString('value=', $output); + } + + public function testValueZeroAttributeRendered() + { + $control = new TProgress(); + $control->setValue(0); + $output = $this->render($control); + $this->assertStringContainsString('value="0"', $output); + } + + public function testMaxAttributeRenderedWhenNotDefault() + { + $control = new TProgress(); + $control->setValue(35); + $control->setMax(100); + $output = $this->render($control); + $this->assertStringContainsString('max="100"', $output); + } + + public function testMaxAttributeNotRenderedWhenDefault() + { + $control = new TProgress(); + $output = $this->render($control); + $this->assertStringNotContainsString('max=', $output); + } + + public function testValueGreaterThanMaxRenderedAsIs() + { + // The HTML specification has the browser clamp display; both attributes render unmodified + $control = new TProgress(); + $control->setValue(5); + $output = $this->render($control); + $this->assertStringContainsString('value="5"', $output); + $this->assertStringNotContainsString('max=', $output); + } + + public function testValueDecimalPrecisionRendered() + { + $control = new TProgress(); + $control->setValue('0.33'); + $output = $this->render($control); + $this->assertStringContainsString('value="0.33"', $output); + } + + public function testMaxResetToDefaultOmitsAttribute() + { + $control = new TProgress(); + $control->setMax(100); + $control->setMax(1.0); + $output = $this->render($control); + $this->assertStringNotContainsString('max=', $output); + } + + public function testNotVisibleRendersNothing() + { + // Visibility is enforced by renderControl(), the framework's render entry point + $control = new TProgress(); + $control->setValue(0.5); + $control->setVisible(false); + $textWriter = new \Prado\IO\TTextWriter(); + $control->renderControl(new \Prado\Web\UI\THtmlWriter($textWriter)); + $this->assertSame('', $textWriter->flush()); + } + + public function testCssClassAndAttributesRendered() + { + $control = new TProgress(); + $control->setCssClass('upload-bar'); + $control->getAttributes()->add('data-task', 'upload'); + $output = $this->render($control); + $this->assertStringContainsString('class="upload-bar"', $output); + $this->assertStringContainsString('data-task="upload"', $output); + } + + public function testFallbackChildContentRendered() + { + $control = new TProgress(); + $control->setValue(35); + $control->setMax(100); + $control->getControls()->add('35%'); + $output = $this->render($control); + $this->assertStringContainsString('>35%', $output); + } +}