diff --git a/demos/_unit-test/dropdown-html.php b/demos/_unit-test/dropdown-html.php new file mode 100644 index 0000000000..667c7e2b81 --- /dev/null +++ b/demos/_unit-test/dropdown-html.php @@ -0,0 +1,112 @@ + $v . ' "\' <'; +$htmlValues = [ + $makeTestStringFx('d') => $makeTestStringFx('dTitle'), + $makeTestStringFx('u') => $makeTestStringFx('uTitle'), +]; + +$form = Form::addTo($app); + +$form->addControl('dropdown_single', [ + Form\Control\Dropdown::class, + 'caption' => 'Dropdown single', + 'values' => $htmlValues, +]); + +$form->addControl('dropdown_single2', [ + Form\Control\Dropdown::class, + 'caption' => 'Dropdown single allow addition', + 'values' => $htmlValues, + 'dropdownOptions' => ['allowAdditions' => true], +]); + +$form->addControl('dropdown_multi', [ + Form\Control\Dropdown::class, + 'caption' => 'Dropdown multiple', + 'multiple' => true, + 'values' => $htmlValues, +]); + +$form->addControl('dropdown_multi2', [ + Form\Control\Dropdown::class, + 'caption' => 'Dropdown multiple allow addition', + 'multiple' => true, + 'values' => $htmlValues, + 'dropdownOptions' => ['allowAdditions' => true], +]); + +$lookupModel = new Model(); +$lookupModel->addField('id', ['type' => 'string']); +$lookupModel->addField('name', ['type' => 'string']); +$lookupModel->setPersistence(new Persistence\Array_(array_combine( + array_keys($htmlValues), + array_map(static fn ($v) => ['name' => $v], $htmlValues) +))); + +/* $form->addControl('lookup_single', [ + Form\Control\Lookup::class, + 'caption' => 'Lookup single', + 'model' => $lookupModel, +]); + +$form->addControl('lookup_single2', [ + Form\Control\Lookup::class, + 'caption' => 'Lookup single allow addition', + 'model' => $lookupModel, + 'settings' => ['allowAdditions' => true], +]); + +$form->addControl('lookup_multi', [ + Form\Control\Lookup::class, + 'caption' => 'Lookup multiple', + 'multiple' => true, + 'model' => $lookupModel, +]); + +$form->addControl('lookup_multi2', [ + Form\Control\Lookup::class, + 'caption' => 'Lookup multiple allow addition', + 'multiple' => true, + 'model' => $lookupModel, + 'settings' => ['allowAdditions' => true], +]); */ + +foreach (array_keys($form->entity->getFields()) as $k) { + $form->entity->set($k, $makeTestStringFx('d')); +} + +$initData = $form->entity->get(); + +$form->onSubmit(static function (Form $form) use ($app, $initData, $makeTestStringFx) { + $message = $app->encodeJson($form->entity->get()); + + // TODO remove once https://github.com/fomantic/Fomantic-UI/pull/3205 is merged + foreach ($form->entity->get() as $k => $v) { + $form->entity->set($k, str_replace('"', '"', $v)); + } + + $view = new Message('Values:'); + $view->setApp($form->getApp()); + $view->invokeInit(); + $view->text->addParagraph($message); + $view->text->addParagraph('match init: ' . ($form->entity->get() === $initData)); + $view->text->addParagraph('match u add: ' . ($form->entity->get() === array_map(static fn ($k) => (str_contains($k, 'multi') ? $initData[$k] . ',' : '') . $makeTestStringFx('u'), array_combine(array_keys($initData), array_keys($initData))))); + $view->text->addParagraph('match empty: ' . ($form->entity->get() === array_map(static fn () => '', $initData))); + $view->text->addParagraph('match u only: ' . ($form->entity->get() === array_map(static fn () => $makeTestStringFx('u'), $initData))); + + return $view; +}); diff --git a/src/Behat/Context.php b/src/Behat/Context.php index 1cfad1ab6d..0c5932e2d4 100644 --- a/src/Behat/Context.php +++ b/src/Behat/Context.php @@ -216,6 +216,13 @@ protected function assertNoDuplicateId(): void } } + private function quoteXpath(string $value): string + { + return str_contains($value, '\'') + ? 'concat(\'' . str_replace('\'', '\', "\'", \'', $value) . '\')' + : '\'' . $value . '\''; + } + /** * @return array{ 'css'|'xpath', string } */ @@ -271,27 +278,35 @@ protected function findElement(?NodeElement $context, string $selector): NodeEle return $elements[0]; } - protected function unquoteStepArgument(string $argument): string + protected function unquoteStepArgument(string $value): string { - // copied from https://github.com/Behat/MinkExtension/blob/v2.2/src/Behat/MinkExtension/Context/MinkContext.php#L567 - return str_replace('\"', '"', $argument); + assert(str_starts_with($value, '"') && str_ends_with($value, '"')); + $res = substr($value, 1, -1); + + // based on https://github.com/Behat/MinkExtension/blob/v2.2/src/Behat/MinkExtension/Context/MinkContext.php#L567 + return str_replace(['\\\\', '\"'], ['\\', '"'], $res); } /** * Sleep for a certain time in ms. * - * @When I wait :arg1 ms + * @When ~^I wait ("(?:\\[\\"]|[^"])*+") ms$~ */ - public function iWait(int $ms): void + public function iWait(string $ms): void { + $ms = (int) $this->unquoteStepArgument($ms); + $this->getSession()->wait($ms); } /** - * @When I write :arg1 into selector :selector + * @When ~^I write ("(?:\\[\\"]|[^"])*+") into selector ("(?:\\[\\"]|[^"])*+")$~ */ public function iPressWrite(string $text, string $selector): void { + $text = $this->unquoteStepArgument($text); + $selector = $this->unquoteStepArgument($selector); + if ($selector === 'document' && $text === '[escape]') { $this->getSession()->executeScript('document.dispatchEvent(new KeyboardEvent(\'keydown\', {keyCode: 27, which: 27}))'); @@ -303,10 +318,13 @@ public function iPressWrite(string $text, string $selector): void } /** - * @When I drag selector :selector onto selector :selectorTarget + * @When ~^I drag selector ("(?:\\[\\"]|[^"])*+") onto selector ("(?:\\[\\"]|[^"])*+")$~ */ public function iDragElementOnto(string $selector, string $selectorTarget): void { + $selector = $this->unquoteStepArgument($selector); + $selectorTarget = $this->unquoteStepArgument($selectorTarget); + $elem = $this->findElement(null, $selector); $elemTarget = $this->findElement(null, $selectorTarget); $this->getSession()->getDriver()->dragTo($elem->getXpath(), $elemTarget->getXpath()); @@ -315,28 +333,34 @@ public function iDragElementOnto(string $selector, string $selectorTarget): void // {{{ button /** - * @When I press button :arg1 + * @When ~^I press button ("(?:\\[\\"]|[^"])*+")$~ */ public function iPressButton(string $buttonLabel): void { - $button = $this->findElement(null, '//div[text()="' . $buttonLabel . '"]'); + $buttonLabel = $this->unquoteStepArgument($buttonLabel); + + $button = $this->findElement(null, '//div[text()=' . $this->quoteXpath($buttonLabel) . ']'); $button->click(); } /** - * @Then I see button :arg1 + * @Then ~^I see button ("(?:\\[\\"]|[^"])*+")$~ */ public function iSeeButton(string $buttonLabel): void { - $this->findElement(null, '//div[text()="' . $buttonLabel . '"]'); + $buttonLabel = $this->unquoteStepArgument($buttonLabel); + + $this->findElement(null, '//div[text()=' . $this->quoteXpath($buttonLabel) . ']'); } /** - * @Then I don't see button :arg1 + * @Then ~^I don't see button ("(?:\\[\\"]|[^"])*+")$~ */ public function idontSeeButton(string $text): void { - $element = $this->findElement(null, '//div[text()="' . $text . '"]'); + $text = $this->unquoteStepArgument($text); + + $element = $this->findElement(null, '//div[text()=' . $this->quoteXpath($text) . ']'); if (!str_contains($element->getAttribute('style'), 'display: none')) { throw new \Exception('Element with text "' . $text . '" must be invisible'); } @@ -347,18 +371,22 @@ public function idontSeeButton(string $text): void // {{{ link /** - * @Given I click link :arg1 + * @Given ~^I click link ("(?:\\[\\"]|[^"])*+")$~ */ public function iClickLink(string $label): void { - $this->findElement(null, '//a[text()="' . $label . '"]')->click(); + $label = $this->unquoteStepArgument($label); + + $this->findElement(null, '//a[text()=' . $this->quoteXpath($label) . ']')->click(); } /** - * @When I click using selector :selector + * @When ~^I click using selector ("(?:\\[\\"]|[^"])*+")$~ */ public function iClickUsingSelector(string $selector): void { + $selector = $this->unquoteStepArgument($selector); + $element = $this->findElement(null, $selector); $element->click(); } @@ -368,10 +396,12 @@ public function iClickUsingSelector(string $selector): void * * One solution can be waiting for AJAX after each \WebDriver\AbstractWebDriver::curl() call. * - * @When PATCH DRIVER I click using selector :selector + * @When ~^PATCH DRIVER I click using selector ("(?:\\[\\"]|[^"])*+")$~ */ public function iClickPatchedUsingSelector(string $selector): void { + $selector = $this->unquoteStepArgument($selector); + $element = $this->findElement(null, $selector); $driver = $this->getSession()->getDriver(); @@ -384,19 +414,24 @@ public function iClickPatchedUsingSelector(string $selector): void } /** - * @When I click paginator page :arg1 + * @When ~^I click paginator page ("(?:\\[\\"]|[^"])*+")$~ */ public function iClickPaginatorPage(string $pageNumber): void { + $pageNumber = $this->unquoteStepArgument($pageNumber); + $element = $this->findElement(null, 'a.item[data-page="' . $pageNumber . '"]'); $element->click(); } /** - * @When I fill field using :selector with :value + * @When ~^I fill field using ("(?:\\[\\"]|[^"])*+") with ("(?:\\[\\"]|[^"])*+")$~ */ public function iFillField(string $selector, string $value): void { + $selector = $this->unquoteStepArgument($selector); + $value = $this->unquoteStepArgument($value); + $element = $this->findElement(null, $selector); $element->setValue($value); } @@ -406,43 +441,47 @@ public function iFillField(string $selector, string $value): void // {{{ modal /** - * @When I press Modal button :arg + * @When ~^I press Modal button ("(?:\\[\\"]|[^"])*+")$~ */ public function iPressModalButton(string $buttonLabel): void { + $buttonLabel = $this->unquoteStepArgument($buttonLabel); + $modal = $this->findElement(null, '.modal.visible.active.front'); - $button = $this->findElement($modal, '//div[text()="' . $buttonLabel . '"]'); + $button = $this->findElement($modal, '//div[text()=' . $this->quoteXpath($buttonLabel) . ']'); $button->click(); } /** - * @Then Modal is open with text :arg1 - * @Then Modal is open with text :arg1 in selector :arg2 + * @Then ~^Modal is open with text ("(?:\\[\\"]|[^"])*+")$~ + * @Then ~^Modal is open with text ("(?:\\[\\"]|[^"])*+") in selector ("(?:\\[\\"]|[^"])*+")$~ * * Check if text is present in modal or dynamic modal. */ - public function modalIsOpenWithText(string $text, string $selector = '*'): void + public function modalIsOpenWithText(string $text, string $selector = '"*"'): void { - $textEncoded = str_contains($text, '"') - ? 'concat("' . str_replace('"', '", \'"\', "', $text) . '")' - : '"' . $text . '"'; + $text = $this->unquoteStepArgument($text); + $selector = $this->unquoteStepArgument($selector); $modal = $this->findElement(null, '.modal.visible.active.front'); - $this->findElement($modal, '//' . $selector . '[text()[normalize-space()=' . $textEncoded . ']]'); + $this->findElement($modal, '//' . $selector . '[text()[normalize-space()=' . $this->quoteXpath($text) . ']]'); } /** - * @When I fill Modal field :arg1 with :arg2 + * @When ~^I fill Modal field ("(?:\\[\\"]|[^"])*+") with ("(?:\\[\\"]|[^"])*+")$~ */ public function iFillModalField(string $fieldName, string $value): void { + $fieldName = $this->unquoteStepArgument($fieldName); + $value = $this->unquoteStepArgument($value); + $modal = $this->findElement(null, '.modal.visible.active.front'); $field = $modal->find('named', ['field', $fieldName]); $field->setValue($value); } /** - * @When I click close modal + * @When ~^I click close modal$~ */ public function iClickCloseModal(): void { @@ -452,7 +491,7 @@ public function iClickCloseModal(): void } /** - * @When I hide js modal + * @When ~^I hide js modal$~ */ public function iHideJsModal(): void { @@ -465,7 +504,7 @@ public function iHideJsModal(): void // {{{ panel /** - * @Then Panel is open + * @Then ~^Panel is open$~ */ public function panelIsOpen(): void { @@ -473,32 +512,40 @@ public function panelIsOpen(): void } /** - * @Then Panel is open with text :arg1 - * @Then Panel is open with text :arg1 in selector :arg2 + * @Then ~^Panel is open with text ("(?:\\[\\"]|[^"])*+")$~ + * @Then ~^Panel is open with text ("(?:\\[\\"]|[^"])*+") in selector ("(?:\\[\\"]|[^"])*+")$~ */ - public function panelIsOpenWithText(string $text, string $selector = '*'): void + public function panelIsOpenWithText(string $text, string $selector = '"*"'): void { + $text = $this->unquoteStepArgument($text); + $selector = $this->unquoteStepArgument($selector); + $panel = $this->findElement(null, '.atk-right-panel.atk-visible'); - $this->findElement($panel, '//' . $selector . '[text()[normalize-space()="' . $text . '"]]'); + $this->findElement($panel, '//' . $selector . '[text()[normalize-space()=' . $this->quoteXpath($text) . ']]'); } /** - * @When I fill Panel field :arg1 with :arg2 + * @When ~^I fill Panel field ("(?:\\[\\"]|[^"])*+") with ("(?:\\[\\"]|[^"])*+")$~ */ public function iFillPanelField(string $fieldName, string $value): void { + $fieldName = $this->unquoteStepArgument($fieldName); + $value = $this->unquoteStepArgument($value); + $panel = $this->findElement(null, '.atk-right-panel.atk-visible'); $field = $panel->find('named', ['field', $fieldName]); $field->setValue($value); } /** - * @When I press Panel button :arg + * @When ~^I press Panel button ("(?:\\[\\"]|[^"])*+")$~ */ public function iPressPanelButton(string $buttonLabel): void { + $buttonLabel = $this->unquoteStepArgument($buttonLabel); + $panel = $this->findElement(null, '.atk-right-panel.atk-visible'); - $button = $this->findElement($panel, '//div[text()="' . $buttonLabel . '"]'); + $button = $this->findElement($panel, '//div[text()=' . $this->quoteXpath($buttonLabel) . ']'); $button->click(); } @@ -507,20 +554,24 @@ public function iPressPanelButton(string $buttonLabel): void // {{{ tab /** - * @When I click tab with title :arg1 + * @When ~^I click tab with title ("(?:\\[\\"]|[^"])*+")$~ */ public function iClickTabWithTitle(string $tabTitle): void { + $tabTitle = $this->unquoteStepArgument($tabTitle); + $tabMenu = $this->findElement(null, '.ui.tabular.menu'); - $link = $this->findElement($tabMenu, '//div[text()="' . $tabTitle . '"]'); + $link = $this->findElement($tabMenu, '//div[text()=' . $this->quoteXpath($tabTitle) . ']'); $link->click(); } /** - * @Then Active tab should be :arg1 + * @Then ~^Active tab should be ("(?:\\[\\"]|[^"])*+")$~ */ public function activeTabShouldBe(string $title): void { + $title = $this->unquoteStepArgument($title); + $tab = $this->findElement(null, '.ui.tabular.menu > .item.active'); if ($tab->getText() !== $title) { throw new \Exception('Active tab is not ' . $title); @@ -532,7 +583,7 @@ public function activeTabShouldBe(string $title): void // {{{ input /** - * @Then ~^input "([^"]*)" value should start with "([^"]*)"$~ + * @Then ~^input ("(?:\\[\\"]|[^"])*+") value should start with ("(?:\\[\\"]|[^"])*+")$~ */ public function inputValueShouldStartWith(string $inputName, string $text): void { @@ -547,22 +598,29 @@ public function inputValueShouldStartWith(string $inputName, string $text): void } /** - * @When I search grid for :arg1 + * @When ~^I search grid for ("(?:\\[\\"]|[^"])*+")$~ */ public function iSearchGridFor(string $text): void { + $text = $this->unquoteStepArgument($text); + $search = $this->findElement(null, 'input.atk-grid-search'); $search->setValue($text); } /** - * @When I select value :arg1 in lookup :arg2 + * TODO better method name, it selects name/title, not value. + * + * @When ~^I select value ("(?:\\[\\"]|[^"])*+") in lookup ("(?:\\[\\"]|[^"])*+")$~ */ public function iSelectValueInLookup(string $value, string $inputName): void { + $value = $this->unquoteStepArgument($value); + $inputName = $this->unquoteStepArgument($inputName); + // get dropdown item from Fomantic-UI which is direct parent of input HTML element $isSelectorXpath = $this->parseSelector($inputName)[0] === 'xpath'; - $lookupElem = $this->findElement(null, ($isSelectorXpath ? $inputName : '//input[@name="' . $inputName . '"]') . '/parent::div'); + $lookupElem = $this->findElement(null, ($isSelectorXpath ? $inputName : '//input[@name=' . $this->quoteXpath($inputName) . ']') . '/parent::div'); if ($value === '') { $this->findElement($lookupElem, 'i.remove.icon')->click(); @@ -570,12 +628,13 @@ public function iSelectValueInLookup(string $value, string $inputName): void return; } - // open dropdown and wait till fully opened (just a click is not triggering it) + // open dropdown and wait till fully opened + $this->findElement($lookupElem, 'i.dropdown.icon')->click(); // TODO remove once https://github.com/fomantic/Fomantic-UI/issues/3204 is fixed $this->getSession()->executeScript('$(arguments[0]).dropdown(\'show\')', [$lookupElem]); $this->jqueryWait('$(arguments[0]).hasClass(\'visible\')', [$lookupElem]); // select value - $valueElem = $this->findElement($lookupElem, '//div[text()="' . $value . '"]'); + $valueElem = $this->findElement($lookupElem, '//div.menu//div.item[text()=' . $this->quoteXpath($value) . ']'); $this->getSession()->executeScript('$(arguments[0]).dropdown(\'set selected\', arguments[1]);', [$lookupElem, $valueElem->getAttribute('data-value')]); $this->jqueryWait(); @@ -585,11 +644,15 @@ public function iSelectValueInLookup(string $value, string $inputName): void } /** - * @When I select file input :arg1 with :arg2 as :arg3 + * @When ~^I select file input ("(?:\\[\\"]|[^"])*+") with ("(?:\\[\\"]|[^"])*+") as ("(?:\\[\\"]|[^"])*+")$~ */ public function iSelectFile(string $inputName, string $fileContent, string $fileName): void { - $element = $this->findElement(null, '//input[@name="' . $inputName . '" and @type="hidden"]/following-sibling::input[@type="file"]'); + $inputName = $this->unquoteStepArgument($inputName); + $fileContent = $this->unquoteStepArgument($fileContent); + $fileName = $this->unquoteStepArgument($fileName); + + $element = $this->findElement(null, '//input[@name=' . $this->quoteXpath($inputName) . ' and @type="hidden"]/following-sibling::input[@type="file"]'); $this->getSession()->executeScript(<<<'EOF' const dataTransfer = new DataTransfer(); dataTransfer.items.add(new File([new Uint8Array(arguments[1])], arguments[2])); @@ -606,7 +669,7 @@ private function getScopeBuilderRuleElem(string $ruleName): NodeElement /** * Generic ScopeBuilder rule with select operator and input value. * - * @Then ~^rule "([^"]*)" operator is "([^"]*)" and value is "([^"]*)"$~ + * @Then ~^rule ("(?:\\[\\"]|[^"])*+") operator is ("(?:\\[\\"]|[^"])*+") and value is ("(?:\\[\\"]|[^"])*+")$~ */ public function scopeBuilderRule(string $name, string $operator, string $value): void { @@ -622,7 +685,7 @@ public function scopeBuilderRule(string $name, string $operator, string $value): /** * HasOne reference or enum type rule for ScopeBuilder. * - * @Then ~^reference rule "([^"]*)" operator is "([^"]*)" and value is "([^"]*)"$~ + * @Then ~^reference rule ("(?:\\[\\"]|[^"])*+") operator is ("(?:\\[\\"]|[^"])*+") and value is ("(?:\\[\\"]|[^"])*+")$~ */ public function scopeBuilderReferenceRule(string $name, string $operator, string $value): void { @@ -638,7 +701,7 @@ public function scopeBuilderReferenceRule(string $name, string $operator, string /** * HasOne select or enum type rule for ScopeBuilder. * - * @Then ~^select rule "([^"]*)" operator is "([^"]*)" and value is "([^"]*)"$~ + * @Then ~^select rule ("(?:\\[\\"]|[^"])*+") operator is ("(?:\\[\\"]|[^"])*+") and value is ("(?:\\[\\"]|[^"])*+")$~ */ public function scopeBuilderSelectRule(string $name, string $operator, string $value): void { @@ -654,7 +717,7 @@ public function scopeBuilderSelectRule(string $name, string $operator, string $v /** * Date, Time or Datetime rule for ScopeBuilder. * - * @Then ~^date rule "([^"]*)" operator is "([^"]*)" and value is "([^"]*)"$~ + * @Then ~^date rule ("(?:\\[\\"]|[^"])*+") operator is ("(?:\\[\\"]|[^"])*+") and value is ("(?:\\[\\"]|[^"])*+")$~ */ public function scopeBuilderDateRule(string $name, string $operator, string $value): void { @@ -670,7 +733,7 @@ public function scopeBuilderDateRule(string $name, string $operator, string $val /** * Boolean type rule for ScopeBuilder. * - * @Then ~^bool rule "([^"]*)" has value "([^"]*)"$~ + * @Then ~^bool rule ("(?:\\[\\"]|[^"])*+") has value ("(?:\\[\\"]|[^"])*+")$~ */ public function scopeBuilderBoolRule(string $name, string $value): void { @@ -686,7 +749,7 @@ public function scopeBuilderBoolRule(string $name, string $value): void } /** - * @Then ~^I check if input value for "([^"]*)" match text in "([^"]*)"$~ + * @Then ~^I check if input value for ("(?:\\[\\"]|[^"])*+") match text in ("(?:\\[\\"]|[^"])*+")$~ */ public function compareInputValueText(string $compareSelector, string $compareToSelector): void { @@ -699,7 +762,7 @@ public function compareInputValueText(string $compareSelector, string $compareTo } /** - * @Then ~^I check if input value for "([^"]*)" match text "([^"]*)"$~ + * @Then ~^I check if input value for ("(?:\\[\\"]|[^"])*+") match text ("(?:\\[\\"]|[^"])*+")$~ */ public function compareInputValueToText(string $selector, string $text): void { @@ -717,30 +780,35 @@ public function compareInputValueToText(string $selector, string $text): void // {{{ misc /** - * @Then dump :arg1 + * @Then ~^dump ("(?:\\[\\"]|[^"])*+")$~ */ public function dump(string $arg1): void { - $element = $this->getSession()->getPage()->find('xpath', '//div[text()="' . $arg1 . '"]'); + $arg1 = $this->unquoteStepArgument($arg1); + + $element = $this->getSession()->getPage()->find('xpath', '//div[text()=' . $this->quoteXpath($arg1) . ']'); var_dump($element->getOuterHtml()); } /** - * @When I click filter column name :arg1 + * @When ~^I click filter column name ("(?:\\[\\"]|[^"])*+")$~ */ public function iClickFilterColumnName(string $columnName): void { + $columnName = $this->unquoteStepArgument($columnName); + $column = $this->findElement(null, "th[data-column='" . $columnName . "']"); $icon = $this->findElement($column, 'i'); $icon->click(); } /** - * @Then ~^container "([^"]*)" should display "([^"]*)" item\(s\)$~ + * @Then ~^container ("(?:\\[\\"]|[^"])*+") should display ("(?:\\[\\"]|[^"])*+") item\(s\)$~ */ - public function containerShouldHaveNumberOfItem(string $selector, int $numberOfitems): void + public function containerShouldHaveNumberOfItem(string $selector, string $numberOfitems): void { $selector = $this->unquoteStepArgument($selector); + $numberOfitems = (int) $this->unquoteStepArgument($numberOfitems); $items = $this->getSession()->getPage()->findAll('css', $selector); $count = 0; @@ -753,7 +821,7 @@ public function containerShouldHaveNumberOfItem(string $selector, int $numberOfi } /** - * @When I scroll to top + * @When ~^I scroll to top$~ */ public function iScrollToTop(): void { @@ -761,7 +829,7 @@ public function iScrollToTop(): void } /** - * @When I scroll to bottom + * @When ~^I scroll to bottom$~ */ public function iScrollToBottom(): void { @@ -769,7 +837,7 @@ public function iScrollToBottom(): void } /** - * @Then Toast display should contain text :arg1 + * @Then ~^Toast display should contain text ("(?:\\[\\"]|[^"])*+")$~ */ public function toastDisplayShouldContainText(string $text): void { @@ -783,7 +851,7 @@ public function toastDisplayShouldContainText(string $text): void } /** - * @Then No toast should be displayed + * @Then ~^No toast should be displayed$~ */ public function noToastShouldBeDisplayed(): void { @@ -797,7 +865,7 @@ public function noToastShouldBeDisplayed(): void * Remove once https://github.com/Behat/MinkExtension/pull/386 and * https://github.com/minkphp/Mink/issues/656 are fixed and released. * - * @Then ~^PATCH MINK the (?i)url(?-i) should match "((?:[^"]|\\")*)"$~ + * @Then ~^PATCH MINK the URL should match ("(?:\\[\\"]|[^"])*+")$~ */ public function assertUrlRegExp(string $pattern): void { @@ -807,7 +875,7 @@ public function assertUrlRegExp(string $pattern): void } /** - * @Then ~^I check if text in "([^"]*)" match text in "([^"]*)"$~ + * @Then ~^I check if text in ("(?:\\[\\"]|[^"])*+") match text in ("(?:\\[\\"]|[^"])*+")$~ */ public function compareElementText(string $compareSelector, string $compareToSelector): void { @@ -820,7 +888,7 @@ public function compareElementText(string $compareSelector, string $compareToSel } /** - * @Then ~^I check if text in "([^"]*)" match text "([^"]*)"$~ + * @Then ~^I check if text in ("(?:\\[\\"]|[^"])*+") match text ("(?:\\[\\"]|[^"])*+")$~ */ public function textInContainerShouldMatch(string $selector, string $text): void { @@ -833,7 +901,7 @@ public function textInContainerShouldMatch(string $selector, string $text): void } /** - * @Then ~^I check if text in "([^"]*)" match regex "([^"]*)"$~ + * @Then ~^I check if text in ("(?:\\[\\"]|[^"])*+") match regex ("(?:\\[\\"]|[^"])*+")$~ */ public function textInContainerShouldMatchRegex(string $selector, string $regex): void { @@ -846,10 +914,14 @@ public function textInContainerShouldMatchRegex(string $selector, string $regex) } /** - * @Then Element :arg1 attribute :arg2 should contain text :arg3 + * @Then ~^Element ("(?:\\[\\"]|[^"])*+") attribute ("(?:\\[\\"]|[^"])*+") should contain text ("(?:\\[\\"]|[^"])*+")$~ */ public function elementAttributeShouldContainText(string $selector, string $attribute, string $text): void { + $selector = $this->unquoteStepArgument($selector); + $attribute = $this->unquoteStepArgument($attribute); + $text = $this->unquoteStepArgument($text); + $element = $this->findElement(null, $selector); $attr = $element->getAttribute($attribute); if (!str_contains($attr, $text)) { @@ -858,10 +930,13 @@ public function elementAttributeShouldContainText(string $selector, string $attr } /** - * @Then Element :arg1 should contain class :arg2 + * @Then ~^Element ("(?:\\[\\"]|[^"])*+") should contain class ("(?:\\[\\"]|[^"])*+")$~ */ public function elementShouldContainClass(string $selector, string $class): void { + $selector = $this->unquoteStepArgument($selector); + $class = $this->unquoteStepArgument($class); + $element = $this->findElement(null, $selector); $classes = explode(' ', $element->getAttribute('class')); if (!in_array($class, $classes, true)) { @@ -870,10 +945,13 @@ public function elementShouldContainClass(string $selector, string $class): void } /** - * @Then Element :arg1 should not contain class :arg2 + * @Then ~^Element ("(?:\\[\\"]|[^"])*+") should not contain class ("(?:\\[\\"]|[^"])*+")$~ */ public function elementShouldNotContainClass(string $selector, string $class): void { + $selector = $this->unquoteStepArgument($selector); + $class = $this->unquoteStepArgument($class); + $element = $this->findElement(null, $selector); $classes = explode(' ', $element->getAttribute('class')); if (in_array($class, $classes, true)) { diff --git a/src/Form/Control/Lookup.php b/src/Form/Control/Lookup.php index ae13d44795..4664807381 100644 --- a/src/Form/Control/Lookup.php +++ b/src/Form/Control/Lookup.php @@ -369,6 +369,15 @@ protected function initDropdown($jsChain): void $settings['clearable'] = true; } + if ($this->entityField !== null && $this->entityField->get() !== null) { + $idField = $this->idField + ?? $this->model->idField; + + $entity = $this->model->loadBy($idField, $this->entityField->get()); + + $settings['values'] = [array_merge($this->renderRow($entity), ['selected' => true])]; + } + $jsChain->dropdown($settings); } @@ -401,16 +410,6 @@ protected function renderView(): void $this->initDropdown($jsChain); - if ($this->entityField !== null && $this->entityField->get() !== null) { - $idField = $this->idField - ?? $this->model->idField; - - $entity = $this->model->loadBy($idField, $this->entityField->get()); - - $row = $this->renderRow($entity); - $jsChain->dropdown('set text', $row['title'], true); - } - $this->js(true, $jsChain); parent::renderView(); diff --git a/tests-behat/callback.feature b/tests-behat/callback.feature index 80cdb307ec..01ca3b5dc3 100644 --- a/tests-behat/callback.feature +++ b/tests-behat/callback.feature @@ -19,4 +19,4 @@ Feature: Callback When I click using selector "(//div.ui.atk-test.button)[1]" Then Modal is open with text "Edit Country" When I press Modal button "Save" - Then Toast display should contain text 'Country action "edit" with "Andorra" entity was executed.' + Then Toast display should contain text "Country action \"edit\" with \"Andorra\" entity was executed." diff --git a/tests-behat/card-deck.feature b/tests-behat/card-deck.feature index c34e0cebe0..275a94cd6c 100644 --- a/tests-behat/card-deck.feature +++ b/tests-behat/card-deck.feature @@ -11,7 +11,7 @@ Feature: CardDeck When I fill in "atk_fp_country__numcode" with "123" When I fill in "atk_fp_country__phonecode" with "1" When I press Modal button "Save" - Then Toast display should contain text 'Country action "add" with "Test" entity was executed.' + Then Toast display should contain text "Country action \"add\" with \"Test\" entity was executed." Scenario: search When I fill in "atk-vue-search" with "united kingdom" @@ -21,18 +21,18 @@ Feature: CardDeck When I press button "Edit" Then Modal is open with text "Edit Country" When I press Modal button "Save" - Then Toast display should contain text 'Country action "edit" with "United Kingdom" entity was executed.' + Then Toast display should contain text "Country action \"edit\" with \"United Kingdom\" entity was executed." # make sure search query stick Then I should see "United Kingdom" Scenario: delete When I press button "Delete" When I press Modal button "Ok" - Then Toast display should contain text 'Country action "delete" with "United Kingdom" entity was executed.' + Then Toast display should contain text "Country action \"delete\" with \"United Kingdom\" entity was executed." Scenario: delete - with unlocked DB When I persist DB changes across requests When I press button "Delete" When I press Modal button "Ok" - Then Toast display should contain text 'Record has been deleted!' + Then Toast display should contain text "Record has been deleted!" Then I should not see "United Kingdom" diff --git a/tests-behat/checkbox.feature b/tests-behat/checkbox.feature index 0a77618015..fd2424389b 100644 --- a/tests-behat/checkbox.feature +++ b/tests-behat/checkbox.feature @@ -3,10 +3,10 @@ Feature: Checkbox Scenario: Given I am on "form-control/checkbox.php" When I press button "Save" - Then Toast display should contain text '{ "test": false, "test_checked": true, "also_checked": true }' + Then Toast display should contain text "{ \"test\": false, \"test_checked\": true, \"also_checked\": true }" When I click using selector "//div.ui.checkbox[not(self::*.checked)][input[@name='test']]" When I click using selector "//div.ui.checkbox.checked[input[@name='test_checked']]" When I click using selector "//div.ui.checkbox.checked[input[@name='also_checked']]" When I press button "Save" - Then Toast display should contain text '{ "test": true, "test_checked": false, "also_checked": false }' + Then Toast display should contain text "{ \"test\": true, \"test_checked\": false, \"also_checked\": false }" diff --git a/tests-behat/crud.feature b/tests-behat/crud.feature index 1c0dd19128..bc1009f6ac 100644 --- a/tests-behat/crud.feature +++ b/tests-behat/crud.feature @@ -11,7 +11,7 @@ Feature: Crud When I fill in "atk_fp_country__numcode" with "123" When I fill in "atk_fp_country__phonecode" with "1" When I press Modal button "Save" - Then Toast display should contain text 'Country action "add" with "Test" entity was executed.' + Then Toast display should contain text "Country action \"add\" with \"Test\" entity was executed." Scenario: search When I search grid for "united kingdom" @@ -30,7 +30,7 @@ Feature: Crud When I fill in "atk_fp_country__numcode" with "123" When I fill in "atk_fp_country__phonecode" with "1" When I press Modal button "Save" - Then Toast display should contain text 'Country action "add" with "Test 2" entity was executed.' + Then Toast display should contain text "Country action \"add\" with \"Test 2\" entity was executed." # TODO add should keep search # related with https://github.com/atk4/ui/issues/526 (list newly added record first) When I search grid for "united kingdo" @@ -39,7 +39,7 @@ Feature: Crud When I press button "Edit" Then Modal is open with text "Edit Country" When I press Modal button "Save" - Then Toast display should contain text 'Country action "edit" with "United Kingdom" entity was executed.' + Then Toast display should contain text "Country action \"edit\" with \"United Kingdom\" entity was executed." # make sure search query stick Then I should see "United Kingdom" @@ -56,13 +56,13 @@ Feature: Crud Then Modal is open with text "Edit Country" When I fill in "atk_fp_country__name" with "My United Kingdom" When I press Modal button "Save" - Then Toast display should contain text 'Record has been saved!' + Then Toast display should contain text "Record has been saved!" Then I should see "My United Kingdom" Scenario: delete When I press button "Delete" When I press Modal button "Ok" - Then Toast display should contain text 'Country action "delete" with "United Kingdom" entity was executed.' + Then Toast display should contain text "Country action \"delete\" with \"United Kingdom\" entity was executed." Then I should not see "United Kingdom" Scenario: search across multiple columns diff --git a/tests-behat/dropdown.feature b/tests-behat/dropdown.feature index 37f7e484c7..de3ded351c 100644 --- a/tests-behat/dropdown.feature +++ b/tests-behat/dropdown.feature @@ -6,7 +6,7 @@ Feature: Dropdown When I select value "Sugar/Sweetened" in lookup "sub_category_id" When I select value "Soda" in lookup "product_id" When I click using selector "(//div[text()='Save'])[2]" - Then Modal is open with text '{ "category_id": "2", "sub_category_id": "9", "product_id": "4" }' in selector "p" + Then Modal is open with text "{ \"category_id\": \"2\", \"sub_category_id\": \"9\", \"product_id\": \"4\" }" in selector "p" When I click close modal Then I should see "Soda" When I select value "Coffee and Tea" in lookup "sub_category_id" @@ -25,6 +25,32 @@ Feature: Dropdown When I select value "" in lookup "multi" Then I check if input value for "input[name='multi']" match text "" + Scenario: dropdown with escaped HTML + Given I am on "_unit-test/dropdown-html.php" + When I press button "Save" + Then Modal is open with text "match init: 1" + When I click close modal + When I select value "uTitle \"' <" in lookup "dropdown_single" + When I select value "uTitle \"' <" in lookup "dropdown_single2" + When I select value "uTitle \"' <" in lookup "dropdown_multi" + When I select value "uTitle \"' <" in lookup "dropdown_multi2" + When I press button "Save" + Then Modal is open with text "match u add: 1" + When I click close modal + When I select value "" in lookup "dropdown_single" + When I select value "" in lookup "dropdown_single2" + When I select value "" in lookup "dropdown_multi" + When I select value "" in lookup "dropdown_multi2" + When I press button "Save" + Then Modal is open with text "match empty: 1" + When I click close modal + When I select value "uTitle \"' <" in lookup "dropdown_single" + When I select value "uTitle \"' <" in lookup "dropdown_single2" + When I select value "uTitle \"' <" in lookup "dropdown_multi" + When I select value "uTitle \"' <" in lookup "dropdown_multi2" + When I press button "Save" + Then Modal is open with text "match u only: 1" + Scenario: dropdown menu Given I am on "basic/menu.php" When I click using selector "//div.ui.dropdown[div[text()='With Callback']]" diff --git a/tests-behat/grid.feature b/tests-behat/grid.feature index e0e6699521..bb70cb2136 100644 --- a/tests-behat/grid.feature +++ b/tests-behat/grid.feature @@ -16,7 +16,7 @@ Feature: Grid Scenario: search no ajax Given I am on "collection/grid.php?no-ajax=1" When I search grid for "kingdom" - Then PATCH MINK the url should match "~_q=kingdom~" + Then PATCH MINK the URL should match "~_q=kingdom~" Then I should see "United Kingdom" Scenario: Checkbox click event must not bubble to row click @@ -35,10 +35,10 @@ Feature: Grid Then No toast should be displayed When I click using selector "//div[@id='grid']//tr[2]//div.ui.dropdown[div[text()='Actions...']]//div.menu/div[text()='Action MenuItem']" Then Toast display should contain text "Clicked Action MenuItem" - Then PATCH MINK the url should match "~_unit-test/grid-rowclick.php$~" + Then PATCH MINK the URL should match "~_unit-test/grid-rowclick.php$~" When I click using selector "//div[@id='grid']//tr[2]//a" Then No toast should be displayed - Then PATCH MINK the url should match "~_unit-test/grid-rowclick.php#test~" + Then PATCH MINK the URL should match "~_unit-test/grid-rowclick.php#test~" Scenario: master checkbox Given I am on "_unit-test/grid-master-checkbox.php" @@ -116,7 +116,7 @@ Feature: Grid When I click paginator page "2" Then I should see "Bahamas" When I click using selector "//tr[td[text()='Bahamas']]//div.ui.button[text()='Say HI']" - Then Toast display should contain text 'Loaded "Bahamas" from ID=16' + Then Toast display should contain text "Loaded \"Bahamas\" from ID=16" Scenario: Row remote action - change row CSS Given I am on "interactive/scroll-grid-container.php" diff --git a/tests-behat/lookup.feature b/tests-behat/lookup.feature index e490f783eb..11e817f1b9 100644 --- a/tests-behat/lookup.feature +++ b/tests-behat/lookup.feature @@ -6,8 +6,8 @@ Feature: Lookup When I select value "Dairy" in lookup "atk_fp_product__product_category_id" # '6f3c91cf51e02fd5' = substr(md5('product_sub_category'), 0, 16) When I select value "Yogourt" in lookup "atk_fp_product__6f3c91cf51e02fd5_id" - When I press modal button "Save" - Then Toast display should contain text 'Dairy - Yogourt' + When I press Modal button "Save" + Then Toast display should contain text "Dairy - Yogourt" Scenario: Testing lookup in VirtualPage Given I am on "_unit-test/lookup-virtual-page.php" @@ -33,5 +33,5 @@ Feature: Lookup When I fill in "atk_fp_country__numcode" with "88" When I fill in "atk_fp_country__phonecode" with "8" When I press Modal button "Save" - Then Toast display should contain text 'Country action "add" with "Plusia" entity was executed.' + Then Toast display should contain text "Country action \"add\" with \"Plusia\" entity was executed." Then I check if text in "//div.text[../input[@name='country2']]" match text "Plusia" diff --git a/tests-behat/modal-error.feature b/tests-behat/modal-error.feature index 78bf81d089..014895df64 100644 --- a/tests-behat/modal-error.feature +++ b/tests-behat/modal-error.feature @@ -13,10 +13,10 @@ Feature: Nested modals /w error handling Scenario: Modal with JS error When I press button "Test Modal load JS error" Then Modal is open with text "API JavaScript Error" - Then Modal is open with text 'Fomantic-UI "modal.onShow" setting cannot be customized outside atk' + Then Modal is open with text "Fomantic-UI \"modal.onShow\" setting cannot be customized outside atk" When I hide js modal When I press button "Test Modal load JS error" - Then Modal is open with text 'Fomantic-UI "modal.onShow" setting cannot be customized outside atk' + Then Modal is open with text "Fomantic-UI \"modal.onShow\" setting cannot be customized outside atk" When I hide js modal Scenario: ModalExecutor with PHP error exception is displayed diff --git a/tests-behat/multiline.feature b/tests-behat/multiline.feature index 7a77aa558a..22fbdd1c02 100644 --- a/tests-behat/multiline.feature +++ b/tests-behat/multiline.feature @@ -10,7 +10,7 @@ Feature: Multiline Then the "div[name=-atk_fp_multiline_item__total_sql]" element should contain "134" Then the "div[name=-atk_fp_multiline_item__total_php]" element should contain "134" When I press button "Save" - Then Toast display should contain text '"atk_fp_multiline_item__box": "67", "atk_fp_multiline_item__total_sql": "134" }' + Then Toast display should contain text "\"atk_fp_multiline_item__box\": \"67\", \"atk_fp_multiline_item__total_sql\": \"134\" }" Scenario: add row When I click using selector "//tfoot//button[i.plus.icon]" @@ -23,17 +23,17 @@ Feature: Multiline Then I check if text in "//tr[3]//div[@name='-atk_fp_multiline_item__total_sql']" match text "15" Then I check if text in "//tr[3]//div[@name='-atk_fp_multiline_item__total_php']" match text "15" When I press button "Save" - Then Toast display should contain text '"atk_fp_multiline_item__box": "5", "atk_fp_multiline_item__total_sql": "15" } ]' + Then Toast display should contain text "\"atk_fp_multiline_item__box\": \"5\", \"atk_fp_multiline_item__total_sql\": \"15\" } ]" Then I should not see "Must not be empty" Scenario: delete row When I click using selector "//tr[3]//input[@type='checkbox']" When I click using selector "//tfoot//button[i.trash.icon]" When I press button "Save" - Then Toast display should contain text '"atk_fp_multiline_item__box": "100", "atk_fp_multiline_item__total_sql": "200" } ]' + Then Toast display should contain text "\"atk_fp_multiline_item__box\": \"100\", \"atk_fp_multiline_item__total_sql\": \"200\" } ]" Scenario: delete all rows When I click using selector "//thead//input[@type='checkbox']" When I click using selector "//tfoot//button[i.trash.icon]" When I press button "Save" - Then Toast display should contain text '[]' + Then Toast display should contain text "[]" diff --git a/tests-behat/radio.feature b/tests-behat/radio.feature index 21059c0335..4909f80d8c 100644 --- a/tests-behat/radio.feature +++ b/tests-behat/radio.feature @@ -3,10 +3,10 @@ Feature: Radio Scenario: Given I am on "form-control/form6.php" When I press button "Save" - Then Toast display should contain text '"enum_d": "male", "enum_r": "male"' - Then Toast display should contain text '"list_d": "1", "list_r": "1"' - Then Toast display should contain text '"int_d": "7 000", "int_r": "7 000"' - Then Toast display should contain text '"string_d": "M", "string_r": "M"' + Then Toast display should contain text "\"enum_d\": \"male\", \"enum_r\": \"male\"" + Then Toast display should contain text "\"list_d\": \"1\", \"list_r\": \"1\"" + Then Toast display should contain text "\"int_d\": \"7 000\", \"int_r\": \"7 000\"" + Then Toast display should contain text "\"string_d\": \"M\", \"string_r\": \"M\"" Then Element "//input[@name='int_r' and @checked='checked']" attribute "value" should contain text "7000" When I select value "female" in lookup "enum_d" @@ -14,35 +14,35 @@ Feature: Radio When I select value "female" in lookup "list_d" When I click using selector "//div.ui.radio[not(self::*.checked)][input[@name='list_r'] and label[text()='female']]" When I press button "Save" - Then Toast display should contain text '"enum_d": "female", "enum_r": "female"' - Then Toast display should contain text '"list_d": "0", "list_r": "0"' - Then Toast display should contain text '"int_d": "7 000", "int_r": "7 000"' - Then Toast display should contain text '"string_d": "M", "string_r": "M"' + Then Toast display should contain text "\"enum_d\": \"female\", \"enum_r\": \"female\"" + Then Toast display should contain text "\"list_d\": \"0\", \"list_r\": \"0\"" + Then Toast display should contain text "\"int_d\": \"7 000\", \"int_r\": \"7 000\"" + Then Toast display should contain text "\"string_d\": \"M\", \"string_r\": \"M\"" When I select value "female" in lookup "int_d" When I click using selector "//div.ui.radio[not(self::*.checked)][input[@name='int_r'] and label[text()='female']]" When I select value "female" in lookup "string_d" When I click using selector "//div.ui.radio[not(self::*.checked)][input[@name='string_r'] and label[text()='female']]" When I press button "Save" - Then Toast display should contain text '"enum_d": "female", "enum_r": "female"' - Then Toast display should contain text '"list_d": "0", "list_r": "0"' - Then Toast display should contain text '"int_d": "5", "int_r": "5"' - Then Toast display should contain text '"string_d": "F", "string_r": "F"' + Then Toast display should contain text "\"enum_d\": \"female\", \"enum_r\": \"female\"" + Then Toast display should contain text "\"list_d\": \"0\", \"list_r\": \"0\"" + Then Toast display should contain text "\"int_d\": \"5\", \"int_r\": \"5\"" + Then Toast display should contain text "\"string_d\": \"F\", \"string_r\": \"F\"" When I select value "" in lookup "enum_d" When I click using selector "//div.ui.radio.checked[input[@name='enum_r']]" When I select value "" in lookup "list_d" When I click using selector "//div.ui.radio.checked[input[@name='list_r']]" When I press button "Save" - Then Toast display should contain text '"enum_d": null, "enum_r": null' - Then Toast display should contain text '"list_d": null, "list_r": null' - Then Toast display should contain text '"int_d": "5", "int_r": "5"' - Then Toast display should contain text '"string_d": "F", "string_r": "F"' + Then Toast display should contain text "\"enum_d\": null, \"enum_r\": null" + Then Toast display should contain text "\"list_d\": null, \"list_r\": null" + Then Toast display should contain text "\"int_d\": \"5\", \"int_r\": \"5\"" + Then Toast display should contain text "\"string_d\": \"F\", \"string_r\": \"F\"" When I select value "" in lookup "int_d" When I click using selector "//div.ui.radio.checked[input[@name='int_r']]" When I select value "" in lookup "string_d" When I click using selector "//div.ui.radio.checked[input[@name='string_r']]" When I press button "Save" - Then Toast display should contain text '"enum_d": null, "enum_r": null' - Then Toast display should contain text '"list_d": null, "list_r": null' - Then Toast display should contain text '"int_d": null, "int_r": null' - Then Toast display should contain text '"string_d": null, "string_r": null' + Then Toast display should contain text "\"enum_d\": null, \"enum_r\": null" + Then Toast display should contain text "\"list_d\": null, \"list_r\": null" + Then Toast display should contain text "\"int_d\": null, \"int_r\": null" + Then Toast display should contain text "\"string_d\": null, \"string_r\": null" diff --git a/tests-behat/remove-observer.feature b/tests-behat/remove-observer.feature index 36a2b887e1..0f94e993a5 100644 --- a/tests-behat/remove-observer.feature +++ b/tests-behat/remove-observer.feature @@ -85,9 +85,9 @@ Feature: Remove observer Scenario: abort SSE when owner is reloaded Given I am on "_unit-test/remove-observer.php" When I press button "Run slow SSE" - When I wait 2000 ms + When I wait "2000" ms Then I should see "Abort failed" Given I am on "_unit-test/remove-observer.php" When I press button "Run slow SSE & remove" - When I wait 2000 ms + When I wait "2000" ms Then I should not see "Abort failed" diff --git a/tests-behat/sortable.feature b/tests-behat/sortable.feature index 59a2f32b2e..0891023328 100644 --- a/tests-behat/sortable.feature +++ b/tests-behat/sortable.feature @@ -16,4 +16,4 @@ Feature: Sortable / Draggable Scenario: drag column resize Given I am on "collection/table2.php" When I drag selector "(//div.grip-resizable)[2]" onto selector "(//div.grip-resizable)[1]" - Then Toast display should contain text 'New widths: { "action": "wide", "amount": "narrow", "amount_copy": "wide" }' + Then Toast display should contain text "New widths: { \"action\": \"wide\", \"amount\": \"narrow\", \"amount_copy\": \"wide\" }" diff --git a/tests-behat/virtual-page.feature b/tests-behat/virtual-page.feature index 68254e3ae1..dcb853f935 100644 --- a/tests-behat/virtual-page.feature +++ b/tests-behat/virtual-page.feature @@ -2,34 +2,34 @@ Feature: VirtualPage Scenario: Given I am on "interactive/virtual.php" - When I click link 'More info on Car' + When I click link "More info on Car" Then I check if text in ".__atk-behat-test-car" match text "Car" When I press button "Open Lorem Ipsum" - Then Modal is open with text 'This is yet another modal' + Then Modal is open with text "This is yet another modal" Scenario: Given I am on "interactive/virtual.php" - When I press button 'Load in Modal' - Then Modal is open with text 'Contents of your pop-up here' + When I press button "Load in Modal" + Then Modal is open with text "Contents of your pop-up here" When I click close modal Scenario: - When I click link 'Inside current layout' + When I click link "Inside current layout" Then I check if text in ".__atk-behat-test-content" match text "Contents of your pop-up here" Scenario: Given I am on "interactive/virtual.php" - When I click link 'On a blank page' + When I click link "On a blank page" Then I check if text in ".__atk-behat-test-content" match text "Contents of your pop-up here" Scenario: Given I am on "_unit-test/virtual-page.php" - When I click link 'Open First' + When I click link "Open First" Then I check if text in ".__atk-behat-test-first" match text "First Level Page" - When I click link 'Open Second' + When I click link "Open Second" Then I check if text in ".__atk-behat-test-second" match text "Second Level Page" - When I click link 'Open Third' + When I click link "Open Third" Then I check if text in ".__atk-behat-test-third" match text "Third Level Page" When I select value "Beverages" in lookup "category" When I press button "Save" - Then Toast display should contain text 'Beverages' + Then Toast display should contain text "Beverages" diff --git a/tests/Behat/ContextTest.php b/tests/Behat/ContextTest.php new file mode 100644 index 0000000000..832d231f90 --- /dev/null +++ b/tests/Behat/ContextTest.php @@ -0,0 +1,75 @@ + + */ + protected function extractPhpdocRegexes(string $file): array + { + $content = file_get_contents($file); + + $res = []; + preg_match_all('~@(Given|Then|When) (.*)~', $content, $matchesAll, \PREG_SET_ORDER); + foreach ($matchesAll as $matches) { + $res[] = $matches[2]; + } + + return $res; + } + + /** + * @dataProvider provideFiles + */ + #[DataProvider('provideFiles')] + public function testFileHasRegexes(string $file): void + { + self::assertGreaterThan(0, count($this->extractPhpdocRegexes($file))); + } + + /** + * @dataProvider provideFiles + */ + #[DataProvider('provideFiles')] + public function testRegexStartAndEnd(string $file): void + { + foreach ($this->extractPhpdocRegexes($file) as $regex) { + self::assertStringStartsWith('~^', $regex); + self::assertStringEndsWith('$~', $regex); + self::assertStringEndsNotWith('.$~', $regex); + } + } + + /** + * @dataProvider provideFiles + */ + #[DataProvider('provideFiles')] + public function testRegexArgumentFormat(string $file): void + { + foreach ($this->extractPhpdocRegexes($file) as $regex) { + preg_match_all('~\((?:\(.*?\)|.)+?\)~', $regex, $matchesAll, \PREG_SET_ORDER); + foreach ($matchesAll as $matches) { + if (!str_contains($matches[0], '"')) { + continue; + } + + self::assertSame('("(?:\\\[\\\"]|[^"])*+")', $matches[0]); + } + } + } + + /** + * @return iterable> + */ + public static function provideFiles(): iterable + { + yield [dirname(__DIR__, 2) . '/src/Behat/Context.php']; + } +}