mirror of
https://github.com/Sylius/Sylius.git
synced 2026-07-05 20:57:12 +00:00
Merge 9ab15561b3 into f2914c3200
This commit is contained in:
commit
98cd2f21ce
34 changed files with 489 additions and 188 deletions
|
|
@ -18,7 +18,6 @@ default:
|
|||
|
||||
extensions:
|
||||
DMore\ChromeExtension\Behat\ServiceContainer\ChromeExtension: ~
|
||||
Robertfausk\Behat\PantherExtension: ~
|
||||
|
||||
FriendsOfBehat\MinkDebugExtension:
|
||||
directory: etc/build
|
||||
|
|
@ -29,7 +28,7 @@ default:
|
|||
files_path: "%paths.base%/src/Sylius/Behat/Resources/fixtures/"
|
||||
base_url: "http://127.0.0.1:8080/"
|
||||
default_session: symfony
|
||||
javascript_session: panther
|
||||
javascript_session: chromedriver
|
||||
sessions:
|
||||
symfony:
|
||||
symfony: ~
|
||||
|
|
@ -37,38 +36,6 @@ default:
|
|||
chrome:
|
||||
api_url: http://127.0.0.1:9222
|
||||
validate_certificate: false
|
||||
chrome_headless_second_session:
|
||||
chrome:
|
||||
api_url: http://127.0.0.1:9222
|
||||
validate_certificate: false
|
||||
panther:
|
||||
panther:
|
||||
manager_options:
|
||||
connection_timeout_in_ms: 5000
|
||||
request_timeout_in_ms: 120000
|
||||
chromedriver_arguments:
|
||||
- --log-path=etc/build/chromedriver.log
|
||||
- --verbose
|
||||
capabilities:
|
||||
acceptSslCerts: true
|
||||
acceptInsecureCerts: true
|
||||
unexpectedAlertBehaviour: accept
|
||||
goog:chromeOptions:
|
||||
args:
|
||||
- --user-data-dir=/tmp/panther-chrome-data
|
||||
- --window-size=1920,1200
|
||||
- --no-sandbox
|
||||
- --disable-dev-shm-usage
|
||||
- --disable-gpu
|
||||
- --disable-infobars
|
||||
- --disable-features=TranslateUI
|
||||
- --disable-translate
|
||||
- --disable-popup-blocking
|
||||
- --disable-blink-features=AutomationControlled
|
||||
- --disable-component-extensions-with-background-pages
|
||||
- --disable-background-networking
|
||||
- --disable-dev-tools
|
||||
- --disable-extensions
|
||||
show_auto: false
|
||||
|
||||
FriendsOfBehat\SymfonyExtension: ~
|
||||
|
|
|
|||
|
|
@ -223,7 +223,6 @@
|
|||
"phpunit/phpunit": "^11.5",
|
||||
"psr/event-dispatcher": "^1.0",
|
||||
"rector/rector": "^2.0",
|
||||
"robertfausk/behat-panther-extension": "^1.1",
|
||||
"sylius-labs/coding-standard": "^4.4",
|
||||
"sylius-labs/suite-tags-extension": "~0.2",
|
||||
"symfony/browser-kit": "^6.4 || ^7.2",
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ use Behat\Step\Then;
|
|||
use Behat\Step\When;
|
||||
use FriendsOfBehat\PageObjectExtension\Page\UnexpectedPageException;
|
||||
use Sylius\Behat\Page\Shop\Checkout\AddressPageInterface;
|
||||
use Sylius\Behat\Page\Shop\Checkout\SelectPaymentPageInterface;
|
||||
use Sylius\Behat\Page\Shop\Checkout\SelectShippingPageInterface;
|
||||
use Sylius\Behat\Service\Factory\AddressFactoryInterface;
|
||||
use Sylius\Behat\Service\Helper\JavaScriptTestHelperInterface;
|
||||
|
|
@ -36,6 +37,7 @@ final readonly class CheckoutAddressingContext implements Context
|
|||
private AddressFactoryInterface $addressFactory,
|
||||
private AddressComparatorInterface $addressComparator,
|
||||
private SelectShippingPageInterface $selectShippingPage,
|
||||
private SelectPaymentPageInterface $selectPaymentPage,
|
||||
private JavaScriptTestHelperInterface $testHelper,
|
||||
) {
|
||||
}
|
||||
|
|
@ -66,6 +68,33 @@ final readonly class CheckoutAddressingContext implements Context
|
|||
*/
|
||||
public function iAmAtTheCheckoutAddressingStep(): void
|
||||
{
|
||||
// Ensure Mink session is started without asserting current URL.
|
||||
$this->addressPage->tryToOpen();
|
||||
|
||||
// If after navigation we're already on Addressing, verify and return.
|
||||
if ($this->addressPage->isOpen()) {
|
||||
$this->addressPage->verify();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If we were redirected to Shipping, use the UI step to go back to Addressing.
|
||||
if ($this->selectShippingPage->isOpen()) {
|
||||
$this->selectShippingPage->changeAddressByStepLabel();
|
||||
$this->addressPage->verify();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Or if we were redirected to Payment, also go back via the UI.
|
||||
if ($this->selectPaymentPage->isOpen()) {
|
||||
$this->selectPaymentPage->changeAddressByStepLabel();
|
||||
$this->addressPage->verify();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// As a last resort, explicitly open the Addressing page (should rarely be needed).
|
||||
$this->addressPage->open();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,17 +16,33 @@ namespace Sylius\Behat\Element\Admin;
|
|||
use Behat\Mink\Element\NodeElement;
|
||||
use Sylius\Behat\Element\SyliusElement;
|
||||
use Sylius\Behat\Service\DriverHelper;
|
||||
use WebDriver\Exception\StaleElementReference;
|
||||
|
||||
class NotificationsElement extends SyliusElement implements NotificationsElementInterface
|
||||
{
|
||||
public function hasNotification(string $type, string $message): bool
|
||||
{
|
||||
$flashesContainer = $this->getElement('flashes_container');
|
||||
$document = $this->getDocument();
|
||||
$flashesContainer = $document->waitFor(5, function () {
|
||||
$container = $this->getDocument()->find('css', '[data-test-sylius-flashes-container]');
|
||||
|
||||
if (DriverHelper::isJavascript($this->getDriver())) {
|
||||
$flashesContainer->waitFor(5, function () use ($flashesContainer) {
|
||||
return $flashesContainer->isVisible();
|
||||
if (!$container instanceof NodeElement) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!DriverHelper::isJavascript($this->getDriver())) {
|
||||
return $container;
|
||||
}
|
||||
|
||||
try {
|
||||
return $container->isVisible() ? $container : false;
|
||||
} catch (StaleElementReference) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (!$flashesContainer instanceof NodeElement) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var array<NodeElement> $flashes */
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ use Behat\Mink\Element\NodeElement;
|
|||
use Sylius\Behat\Behaviour\ChecksCodeImmutability;
|
||||
use Sylius\Behat\Behaviour\SpecifiesItsField;
|
||||
use Sylius\Behat\Element\Admin\Crud\FormElement as BaseFormElement;
|
||||
use Sylius\Behat\Service\DriverHelper;
|
||||
|
||||
class FormElement extends BaseFormElement implements FormElementInterface
|
||||
{
|
||||
|
|
@ -41,7 +42,9 @@ class FormElement extends BaseFormElement implements FormElementInterface
|
|||
|
||||
public function addOptionValue(string $code, string $localeCode, string $value): void
|
||||
{
|
||||
$this->getElement('add_option_value')->press();
|
||||
$addOptionValueButton = $this->getElement('add_option_value');
|
||||
|
||||
DriverHelper::guardedClick($this->getSession(), $addOptionValueButton);
|
||||
$this->waitForFormUpdate();
|
||||
|
||||
$lastValue = $this->getElement('last_option_value');
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ class FormElement extends BaseFormElement implements FormElementInterface
|
|||
|
||||
public function getTranslationFieldValue(string $element, string $localeCode): string
|
||||
{
|
||||
DriverHelper::waitForPageToLoad($this->getSession());
|
||||
DriverHelper::waitForDomSettled($this->getSession());
|
||||
|
||||
return $this->getElement($element, ['%locale_code%' => $localeCode])->getValue();
|
||||
}
|
||||
|
|
@ -152,6 +152,24 @@ class FormElement extends BaseFormElement implements FormElementInterface
|
|||
return;
|
||||
}
|
||||
|
||||
$session = $this->getSession();
|
||||
|
||||
if (DriverHelper::isJavascript($session->getDriver())) {
|
||||
DriverHelper::guardedClick($session, $translationAccordion);
|
||||
DriverHelper::waitForDomSettled($session);
|
||||
|
||||
$targetSelector = $translationAccordion->getAttribute('data-bs-target');
|
||||
if (null !== $targetSelector) {
|
||||
$this->getDocument()->waitFor(5, function () use ($targetSelector) {
|
||||
$content = $this->getDocument()->find('css', $targetSelector);
|
||||
|
||||
return $content instanceof NodeElement && $content->hasClass('show');
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$translationAccordion->click();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ class TreeElement extends SyliusElement implements TreeElementInterface
|
|||
|
||||
public function countTaxons(): int
|
||||
{
|
||||
DriverHelper::waitForPageToLoad($this->getSession());
|
||||
DriverHelper::waitForDomSettled($this->getSession());
|
||||
|
||||
return count($this->getElement('tree_taxons')->findAll('css', '[data-test-tree-taxon]'));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class SaveElement extends SyliusElement implements SaveElementInterface
|
|||
{
|
||||
if (DriverHelper::isJavascript($this->getDriver())) {
|
||||
$this->getDocument()->find('css', 'body')->click();
|
||||
DriverHelper::waitForPageToLoad($this->getSession());
|
||||
DriverHelper::waitForDomSettled($this->getSession());
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -31,7 +31,7 @@ class SaveElement extends SyliusElement implements SaveElementInterface
|
|||
// Fallback for elements with different data-test attributes
|
||||
$this->getElement('save_changes_button')->press();
|
||||
}
|
||||
DriverHelper::waitForPageToLoad($this->getSession());
|
||||
DriverHelper::waitForDomSettled($this->getSession());
|
||||
}
|
||||
|
||||
protected function getDefinedElements(): array
|
||||
|
|
|
|||
|
|
@ -35,9 +35,13 @@ class RegisterElement extends SyliusElement implements RegisterElementInterface
|
|||
|
||||
public function register(): void
|
||||
{
|
||||
if (DriverHelper::isJavascript($this->getDriver())) {
|
||||
DriverHelper::guardedClick($this->getSession(), $this->getElement('register_button'));
|
||||
} else {
|
||||
$this->getElement('register_button')->click();
|
||||
}
|
||||
|
||||
DriverHelper::waitForPageToLoad($this->getSession());
|
||||
DriverHelper::waitForDomSettled($this->getSession());
|
||||
}
|
||||
|
||||
public function specifyEmail(?string $email): void
|
||||
|
|
|
|||
|
|
@ -91,21 +91,48 @@ class UpdatePage extends BaseUpdatePage implements UpdatePageInterface
|
|||
|
||||
public function getFormValidationErrors(): array
|
||||
{
|
||||
$errors = $this->getElement('form')->findAll('css', '.alert-danger');
|
||||
$form = $this->getDocument()->waitFor(5, function () {
|
||||
$form = $this->getDocument()->find('css', 'form');
|
||||
|
||||
return array_map(fn (NodeElement $element) => $element->getText(), $errors);
|
||||
if (!$form instanceof NodeElement) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$hasErrors = $form->has('css', '.alert-danger') || $form->has('css', '[data-test-validation-error]');
|
||||
|
||||
return $hasErrors ? $form : false;
|
||||
});
|
||||
|
||||
if (!$form instanceof NodeElement) {
|
||||
throw new ElementNotFoundException($this->getSession(), 'Form element', 'css', 'form');
|
||||
}
|
||||
|
||||
$messages = [];
|
||||
|
||||
foreach ($form->findAll('css', '.alert-danger') as $alert) {
|
||||
$messages[] = trim($alert->getText());
|
||||
}
|
||||
|
||||
foreach ($form->findAll('css', '[data-test-validation-error]') as $inlineError) {
|
||||
$messages[] = trim($inlineError->getText());
|
||||
}
|
||||
|
||||
return array_values(array_filter($messages, static fn (string $message): bool => $message !== ''));
|
||||
}
|
||||
|
||||
public function getValidationMessage(string $element): string
|
||||
{
|
||||
$province = $this->getElement('last_province');
|
||||
|
||||
$foundElement = $province->find('css', '.invalid-feedback');
|
||||
if (null === $foundElement) {
|
||||
throw new ElementNotFoundException($this->getSession(), 'Tag', 'css', '.invalid-feedback');
|
||||
$foundElement = $province->waitFor(5, static function (NodeElement $province): ?NodeElement {
|
||||
return $province->find('css', '.invalid-feedback');
|
||||
});
|
||||
|
||||
if (!$foundElement instanceof NodeElement) {
|
||||
throw new ElementNotFoundException($this->getSession(), 'Validation message', 'css', '.invalid-feedback');
|
||||
}
|
||||
|
||||
return $foundElement->getText();
|
||||
return trim($foundElement->getText());
|
||||
}
|
||||
|
||||
protected function getToggleableElement(): NodeElement
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class CreatePage extends SyliusPage implements CreatePageInterface
|
|||
$this->waitForFormUpdate();
|
||||
}
|
||||
$this->getDocument()->pressButton('Create');
|
||||
DriverHelper::waitForPageToLoad($this->getSession());
|
||||
DriverHelper::waitForDomSettled($this->getSession());
|
||||
}
|
||||
|
||||
public function getValidationMessage(string $element, array $parameters = []): string
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class UpdatePage extends SyliusPage implements UpdatePageInterface
|
|||
$this->blur();
|
||||
}
|
||||
$this->getDocument()->find('css', '[data-test-update-changes-button]')->click();
|
||||
DriverHelper::waitForPageToLoad($this->getSession());
|
||||
DriverHelper::waitForDomSettled($this->getSession());
|
||||
}
|
||||
|
||||
public function cancelChanges(): void
|
||||
|
|
|
|||
|
|
@ -2,11 +2,7 @@
|
|||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Sylius Sp. z o.o.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
|
@ -44,6 +40,10 @@ class IndexPage extends BaseIndexPage implements IndexPageInterface
|
|||
|
||||
public function filterByProduct(string $productName): void
|
||||
{
|
||||
if (!$this->areFiltersVisible()) {
|
||||
$this->toggleFilters();
|
||||
}
|
||||
|
||||
$this->autocompleteHelper->selectByName(
|
||||
$this->getDriver(),
|
||||
$this->getElement('product_filter')->getXpath(),
|
||||
|
|
|
|||
|
|
@ -341,7 +341,7 @@ class ShowPage extends SyliusPage implements ShowPageInterface
|
|||
|
||||
public function getOrderCurrency(): string
|
||||
{
|
||||
DriverHelper::waitForPageToLoad($this->getSession());
|
||||
DriverHelper::waitForDomSettled($this->getSession());
|
||||
|
||||
return $this->getElement('currency')->getText();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ class IndexPage extends SyliusPage implements IndexPageInterface
|
|||
{
|
||||
$this->getElement('delete_button', ['%full_name%' => $fullName])->press();
|
||||
|
||||
DriverHelper::waitForPageToLoad($this->getSession());
|
||||
DriverHelper::waitForDomSettled($this->getSession());
|
||||
}
|
||||
|
||||
public function setAsDefault(string $fullName): void
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ class LoginPage extends SyliusPage implements LoginPageInterface
|
|||
{
|
||||
$this->getElement('login_button')->click();
|
||||
|
||||
DriverHelper::waitForPageToLoad($this->getSession());
|
||||
DriverHelper::waitForDomSettled($this->getSession());
|
||||
}
|
||||
|
||||
public function specifyPassword(string $password): void
|
||||
|
|
|
|||
|
|
@ -164,11 +164,7 @@ class AddressPage extends ShopPage implements AddressPageInterface
|
|||
{
|
||||
$this->waitForElementUpdate('form');
|
||||
|
||||
try {
|
||||
$this->getElement('login_button')->press();
|
||||
} catch (ElementNotFoundException) {
|
||||
$this->getElement('login_button')->click();
|
||||
}
|
||||
$this->guardedClickElement($this->getElement('login_button'), true);
|
||||
|
||||
$this->waitForLoginAction();
|
||||
}
|
||||
|
|
@ -203,10 +199,10 @@ class AddressPage extends ShopPage implements AddressPageInterface
|
|||
{
|
||||
if (DriverHelper::isJavascript($this->getDriver())) {
|
||||
$this->blur();
|
||||
DriverHelper::waitForPageToLoad($this->getSession());
|
||||
DriverHelper::waitForDomSettled($this->getSession());
|
||||
}
|
||||
$this->getElement('next_step')->press();
|
||||
DriverHelper::waitForPageToLoad($this->getSession());
|
||||
$this->guardedClickElement($this->getElement('next_step'), true);
|
||||
DriverHelper::waitForDomSettled($this->getSession());
|
||||
}
|
||||
|
||||
public function backToStore(): void
|
||||
|
|
@ -400,7 +396,24 @@ class AddressPage extends ShopPage implements AddressPageInterface
|
|||
protected function chooseDifferentAddress(string $type): void
|
||||
{
|
||||
$elem = $this->getElement(sprintf('different_%s_address', $type));
|
||||
$elem->click();
|
||||
$this->guardedClickElement($elem);
|
||||
$this->waitForElementUpdate('form');
|
||||
}
|
||||
|
||||
private function guardedClickElement(NodeElement $element, bool $isButton = false): void
|
||||
{
|
||||
if (DriverHelper::isJavascript($this->getDriver())) {
|
||||
DriverHelper::guardedClick($this->getSession(), $element);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($isButton) {
|
||||
$element->press();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$element->click();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -181,9 +181,19 @@ class CompletePage extends SyliusPage implements CompletePageInterface
|
|||
|
||||
public function confirmOrder(): void
|
||||
{
|
||||
$this->getElement('confirm_button')->press();
|
||||
$session = $this->getSession();
|
||||
|
||||
DriverHelper::waitForPageToLoad($this->getSession());
|
||||
DriverHelper::waitForDomSettled($session);
|
||||
|
||||
$button = $this->getElement('confirm_button');
|
||||
|
||||
if (DriverHelper::isJavascript($session->getDriver())) {
|
||||
DriverHelper::guardedClick($session, $button);
|
||||
} else {
|
||||
$button->press();
|
||||
}
|
||||
|
||||
DriverHelper::waitForDomSettled($session);
|
||||
}
|
||||
|
||||
public function changeAddress(): void
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Sylius\Behat\Page\Shop\Checkout;
|
||||
|
||||
use Behat\Mink\Element\NodeElement;
|
||||
use Behat\Mink\Exception\ElementNotFoundException;
|
||||
use Sylius\Behat\Page\SyliusPage;
|
||||
use Sylius\Behat\Service\DriverHelper;
|
||||
|
|
@ -27,7 +28,7 @@ class SelectPaymentPage extends SyliusPage implements SelectPaymentPageInterface
|
|||
public function selectPaymentMethod(string $paymentMethod): void
|
||||
{
|
||||
if (DriverHelper::isJavascript($this->getDriver())) {
|
||||
DriverHelper::waitForPageToLoad($this->getSession());
|
||||
DriverHelper::waitForDomSettled($this->getSession());
|
||||
$this->getElement('payment_method_select', ['%payment_method%' => $paymentMethod])->click();
|
||||
|
||||
return;
|
||||
|
|
@ -59,8 +60,25 @@ class SelectPaymentPage extends SyliusPage implements SelectPaymentPageInterface
|
|||
|
||||
public function nextStep(): void
|
||||
{
|
||||
$this->getElement('next_step')->press();
|
||||
DriverHelper::waitForPageToLoad($this->getSession());
|
||||
$session = $this->getSession();
|
||||
|
||||
$button = $this->getDocument()->waitFor(5, function () {
|
||||
$element = $this->getDocument()->find('css', '[data-test-next-step]');
|
||||
if ($element instanceof NodeElement) {
|
||||
return $element;
|
||||
}
|
||||
|
||||
$fallback = $this->getDocument()->find('css', 'form button[type="submit"]');
|
||||
|
||||
return $fallback instanceof NodeElement ? $fallback : false;
|
||||
});
|
||||
|
||||
if (!$button instanceof NodeElement) {
|
||||
throw new ElementNotFoundException($session, 'next step button', 'css', '[data-test-next-step]');
|
||||
}
|
||||
|
||||
DriverHelper::guardedClick($session, $button);
|
||||
DriverHelper::waitForDomSettled($session);
|
||||
}
|
||||
|
||||
public function changeShippingMethod(): void
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Sylius\Behat\Page\Shop\Checkout;
|
||||
|
||||
use Behat\Mink\Element\NodeElement;
|
||||
use Behat\Mink\Exception\ElementNotFoundException;
|
||||
use Sylius\Behat\Page\SyliusPage;
|
||||
use Sylius\Behat\Service\DriverHelper;
|
||||
|
|
@ -27,7 +28,7 @@ class SelectShippingPage extends SyliusPage implements SelectShippingPageInterfa
|
|||
public function selectShippingMethod(string $shippingMethod): void
|
||||
{
|
||||
if (DriverHelper::isJavascript($this->getDriver())) {
|
||||
DriverHelper::waitForPageToLoad($this->getSession());
|
||||
DriverHelper::waitForDomSettled($this->getSession());
|
||||
$this->getElement('shipping_method_select', ['%shipping_method%' => $shippingMethod])->click();
|
||||
|
||||
return;
|
||||
|
|
@ -75,8 +76,25 @@ class SelectShippingPage extends SyliusPage implements SelectShippingPageInterfa
|
|||
|
||||
public function nextStep(): void
|
||||
{
|
||||
$this->getElement('next_step')->press();
|
||||
DriverHelper::waitForPageToLoad($this->getSession());
|
||||
$session = $this->getSession();
|
||||
|
||||
$button = $this->getDocument()->waitFor(5, function () {
|
||||
$element = $this->getDocument()->find('css', '[data-test-next-step]');
|
||||
if ($element instanceof NodeElement) {
|
||||
return $element;
|
||||
}
|
||||
|
||||
$fallback = $this->getDocument()->find('css', 'form button[type="submit"]');
|
||||
|
||||
return $fallback instanceof NodeElement ? $fallback : false;
|
||||
});
|
||||
|
||||
if (!$button instanceof NodeElement) {
|
||||
throw new ElementNotFoundException($session, 'next step button', 'css', '[data-test-next-step]');
|
||||
}
|
||||
|
||||
DriverHelper::guardedClick($session, $button);
|
||||
DriverHelper::waitForDomSettled($session);
|
||||
}
|
||||
|
||||
public function changeAddress(): void
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ class ShowPage extends SyliusPage implements ShowPageInterface
|
|||
{
|
||||
$this->getElement('pay_link')->click();
|
||||
|
||||
DriverHelper::waitForPageToLoad($this->getSession());
|
||||
DriverHelper::waitForDomSettled($this->getSession());
|
||||
}
|
||||
|
||||
public function getNotifications(): array
|
||||
|
|
@ -51,7 +51,7 @@ class ShowPage extends SyliusPage implements ShowPageInterface
|
|||
|
||||
public function choosePaymentMethod(string $paymentMethodName): void
|
||||
{
|
||||
DriverHelper::waitForPageToLoad($this->getSession());
|
||||
DriverHelper::waitForDomSettled($this->getSession());
|
||||
|
||||
$paymentMethodElement = $this->getElement('payment_method', ['%name%' => $paymentMethodName]);
|
||||
$paymentMethodElement->selectOption($paymentMethodElement->getAttribute('value'));
|
||||
|
|
@ -71,7 +71,7 @@ class ShowPage extends SyliusPage implements ShowPageInterface
|
|||
|
||||
public function getChosenPaymentMethod(): string
|
||||
{
|
||||
DriverHelper::waitForPageToLoad($this->getSession());
|
||||
DriverHelper::waitForDomSettled($this->getSession());
|
||||
|
||||
$paymentMethodItems = $this->getDocument()->findAll('css', '[data-test-payment-item]');
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class ThankYouPage extends SyliusPage implements ThankYouPageInterface
|
|||
{
|
||||
$this->getElement('payment_method_page')->click();
|
||||
|
||||
DriverHelper::waitForPageToLoad($this->getSession());
|
||||
DriverHelper::waitForDomSettled($this->getSession());
|
||||
}
|
||||
|
||||
public function goToOrderDetailsInAccount(): void
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ class CreatePage extends Page implements CreatePageInterface
|
|||
$this->waitForElementUpdate('add');
|
||||
$this->getElement('add')->press();
|
||||
|
||||
DriverHelper::waitForPageToLoad($this->getSession());
|
||||
DriverHelper::waitForDomSettled($this->getSession());
|
||||
}
|
||||
|
||||
public function getRateValidationMessage(): string
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ abstract class SyliusPage extends BaseSymfonyPage implements SyliusPageInterface
|
|||
{
|
||||
protected function getElement(string $name, array $parameters = []): NodeElement
|
||||
{
|
||||
DriverHelper::waitForPageToLoad($this->getSession());
|
||||
DriverHelper::waitForDomSettled($this->getSession());
|
||||
|
||||
return parent::getElement($name, $parameters);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -412,6 +412,7 @@
|
|||
<argument type="service" id="sylius.behat.factory.address" />
|
||||
<argument type="service" id="sylius.comparator.address" />
|
||||
<argument type="service" id="sylius.behat.page.shop.checkout.select_shipping" />
|
||||
<argument type="service" id="sylius.behat.page.shop.checkout.select_payment" />
|
||||
<argument type="service" id="sylius.behat.java_script_test_helper" />
|
||||
</service>
|
||||
|
||||
|
|
|
|||
|
|
@ -16,8 +16,10 @@ namespace Sylius\Behat\Service;
|
|||
use Behat\Mink\Driver\DriverInterface;
|
||||
use Behat\Mink\Driver\PantherDriver;
|
||||
use Behat\Mink\Driver\Selenium2Driver;
|
||||
use Behat\Mink\Element\NodeElement;
|
||||
use Behat\Mink\Session;
|
||||
use DMore\ChromeDriver\ChromeDriver;
|
||||
use WebDriver\Exception\StaleElementReference;
|
||||
|
||||
abstract class DriverHelper
|
||||
{
|
||||
|
|
@ -59,4 +61,91 @@ abstract class DriverHelper
|
|||
$session->wait($timeout, "document.readyState === 'complete' && !document.querySelector('[data-live-is-loading]')");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the DOM to settle (complete loading, no pending animations, no loading indicators).
|
||||
* This is more robust than waitForPageToLoad for dynamic content updates.
|
||||
*/
|
||||
public static function waitForDomSettled(Session $session, int $timeout = 2000): void
|
||||
{
|
||||
if (!self::isJavascript($session->getDriver())) {
|
||||
return;
|
||||
}
|
||||
|
||||
$session->wait($timeout, <<<JS
|
||||
document.readyState === 'complete'
|
||||
&& !document.querySelector('[data-live-is-loading]')
|
||||
&& !document.querySelector('.loading')
|
||||
&& typeof jQuery !== 'undefined' ? !jQuery.active : true
|
||||
JS);
|
||||
|
||||
// Additional small wait to ensure DOM mutations have settled
|
||||
usleep(50000); // 50ms
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a guarded click that handles stale elements, scrolling, and WebDriver timing issues.
|
||||
* Retries on StaleElementReference and uses JavaScript click as fallback if needed.
|
||||
*/
|
||||
public static function guardedClick(Session $session, NodeElement $element, int $maxRetries = 3): void
|
||||
{
|
||||
if (!self::isJavascript($session->getDriver())) {
|
||||
$element->click();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$attempt = 0;
|
||||
$lastException = null;
|
||||
|
||||
while ($attempt < $maxRetries) {
|
||||
try {
|
||||
// Scroll element into view
|
||||
$session->executeScript(
|
||||
'arguments[0].scrollIntoView({behavior: "instant", block: "center", inline: "center"});',
|
||||
[$element->getXpath()],
|
||||
);
|
||||
|
||||
// Wait a moment for scroll to complete
|
||||
usleep(100000); // 100ms
|
||||
|
||||
// Try standard click first
|
||||
$element->click();
|
||||
|
||||
return;
|
||||
} catch (StaleElementReference $e) {
|
||||
$lastException = $e;
|
||||
++$attempt;
|
||||
|
||||
if ($attempt >= $maxRetries) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Wait before retry
|
||||
usleep(200000); // 200ms
|
||||
} catch (\Exception $e) {
|
||||
// Fallback to JavaScript click for other exceptions
|
||||
try {
|
||||
$session->executeScript('arguments[0].click();', [$element->getXpath()]);
|
||||
|
||||
return;
|
||||
} catch (\Exception $jsException) {
|
||||
$lastException = $jsException;
|
||||
++$attempt;
|
||||
|
||||
if ($attempt >= $maxRetries) {
|
||||
break;
|
||||
}
|
||||
|
||||
usleep(200000); // 200ms
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new \RuntimeException(
|
||||
sprintf('Failed to click element after %d attempts. Last error: %s', $maxRetries, $lastException?->getMessage() ?? 'Unknown'),
|
||||
0,
|
||||
$lastException,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,7 @@
|
|||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Sylius Sp. z o.o.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
|
@ -17,147 +13,222 @@ use Behat\Mink\Driver\DriverInterface;
|
|||
|
||||
final class AutocompleteHelper implements AutocompleteHelperInterface
|
||||
{
|
||||
/**
|
||||
* Zwraca aktualnie wybrane pozycje w komponencie TomSelect jako mapę: [value => label].
|
||||
*/
|
||||
public function getSelectedItems(DriverInterface $driver, string $selector): array
|
||||
{
|
||||
$selector = $this->normalizeSelector($selector);
|
||||
$result = $driver->evaluateScript(<<<SCRIPT
|
||||
$selector = $this->escapeForJsDoubleQuoted($selector);
|
||||
|
||||
$result = $driver->evaluateScript(<<<JS
|
||||
(function () {
|
||||
let select = document.evaluate("//SELECT[{$selector}]", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
||||
let selectedOptions = [];
|
||||
|
||||
[...select.options].forEach((option) => selectedOptions[option.value] = option.textContent);
|
||||
|
||||
return selectedOptions;
|
||||
var el = document.evaluate("{$selector}", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
||||
if (!el) return {};
|
||||
var ts = el.tomselect;
|
||||
var out = {};
|
||||
if (ts) {
|
||||
(ts.items || []).forEach(function (val) {
|
||||
var opt = ts.options[val];
|
||||
out[val] = (opt && (opt.text || opt.title || opt.label)) ? String(opt.text || opt.title || opt.label).trim() : String(val);
|
||||
});
|
||||
} else if (el.options) {
|
||||
for (var i = 0; i < el.options.length; i++) {
|
||||
var opt = el.options[i];
|
||||
if (opt.selected) out[opt.value] = (opt.textContent || '').trim();
|
||||
}
|
||||
}
|
||||
return out;
|
||||
})();
|
||||
SCRIPT);
|
||||
JS);
|
||||
|
||||
return is_array($result) ? $result : [];
|
||||
}
|
||||
|
||||
public function search(DriverInterface $driver, string $selector, string $searchString): mixed
|
||||
/**
|
||||
* Wyszukuje w dropdownie TS — zwraca mapę wyników: [value => label].
|
||||
*/
|
||||
public function search(DriverInterface $driver, string $selector, string $searchString): array
|
||||
{
|
||||
$selector = $this->normalizeSelector($selector);
|
||||
$driver->executeScript(<<<SCRIPT
|
||||
$selector = $this->escapeForJsDoubleQuoted($selector);
|
||||
$search = $this->toJsString($searchString);
|
||||
|
||||
// Poczekaj aż TomSelect się zainicjalizuje
|
||||
$driver->wait(3000, <<<JS
|
||||
(function(){
|
||||
let element = document.evaluate("{$selector}", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
||||
element.tomselect.load('$searchString');
|
||||
element.tomselect.open();
|
||||
var el = document.evaluate("{$selector}", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
||||
return !!(el && el.tomselect);
|
||||
})();
|
||||
SCRIPT);
|
||||
JS);
|
||||
|
||||
$driver->wait(
|
||||
2000,
|
||||
<<<SCRIPT
|
||||
// Otwórz, ustaw frazę — bez klikania ukrytych elementów
|
||||
$driver->executeScript(<<<JS
|
||||
(function(){
|
||||
let element = document.evaluate("{$selector}", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
||||
return element.tomselect.loading === 0;
|
||||
var el = document.evaluate("{$selector}", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
||||
var ts = el.tomselect;
|
||||
el.closest('.ts-wrapper')?.scrollIntoView({block:'center', inline:'center'});
|
||||
ts.open();
|
||||
ts.focus();
|
||||
ts.setTextboxValue({$search});
|
||||
if (typeof ts.onSearchChange === 'function') { ts.onSearchChange({$search}); }
|
||||
})();
|
||||
SCRIPT,
|
||||
);
|
||||
JS);
|
||||
|
||||
return $driver->evaluateScript(<<<SCRIPT
|
||||
// Czekaj aż skończy ładować i dropdown jest otwarty
|
||||
$driver->wait(4000, <<<JS
|
||||
(function(){
|
||||
let element = document.evaluate("{$selector}", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
||||
let searchResults = [];
|
||||
|
||||
element.parentElement.querySelectorAll('[data-selectable]').forEach((node) => searchResults[node.dataset.value] = node.textContent);
|
||||
|
||||
return searchResults;
|
||||
var el = document.evaluate("{$selector}", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
||||
var ts = el && el.tomselect;
|
||||
if (!ts) return false;
|
||||
var open = !!(ts.isOpen || (ts.dropdown && ts.dropdown.classList.contains('ts-dropdown') && !ts.dropdown.classList.contains('hidden')));
|
||||
var notLoading = (ts.loading === 0);
|
||||
return open && notLoading;
|
||||
})();
|
||||
SCRIPT);
|
||||
JS);
|
||||
|
||||
// Zbierz wyniki z dropdownu
|
||||
$result = $driver->evaluateScript(<<<JS
|
||||
(function(){
|
||||
var el = document.evaluate("{$selector}", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
||||
var ts = el.tomselect;
|
||||
var out = {};
|
||||
var c = (ts && ts.dropdown_content) ? ts.dropdown_content : el.parentElement;
|
||||
var nodes = c ? c.querySelectorAll('[data-selectable]') : [];
|
||||
nodes.forEach(function(n){
|
||||
var val = n.getAttribute('data-value');
|
||||
var label = (n.textContent || '').trim();
|
||||
if (val) out[val] = label;
|
||||
});
|
||||
return out;
|
||||
})();
|
||||
JS);
|
||||
|
||||
return is_array($result) ? $result : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Wybierz pozycję po nazwie (najpierw exact, potem contains).
|
||||
*/
|
||||
public function selectByName(DriverInterface $driver, string $selector, string $name): void
|
||||
{
|
||||
$selector = $this->normalizeSelector($selector);
|
||||
$foundItems = array_flip($this->search($driver, $selector, $name));
|
||||
$found = $this->search($driver, $selector, $name);
|
||||
if ($found === []) {
|
||||
throw new \InvalidArgumentException(sprintf('No results returned for "%s".', $name));
|
||||
}
|
||||
|
||||
$value = $this->getValueByPhrase($foundItems, $name);
|
||||
$value = null;
|
||||
foreach ($found as $val => $label) {
|
||||
$label = trim((string)$label);
|
||||
if ($label === $name) { $value = (string)$val; break; }
|
||||
if ($value === null && $label !== '' && str_contains($label, $name)) { $value = (string)$val; }
|
||||
}
|
||||
if ($value === null) {
|
||||
throw new \InvalidArgumentException(sprintf('Could not find "%s" in the autocomplete', $name));
|
||||
}
|
||||
|
||||
$this->addItemByValue($driver, $selector, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Usuń pozycję po nazwie (exact/contains).
|
||||
*/
|
||||
public function removeByName(DriverInterface $driver, string $selector, string $name): void
|
||||
{
|
||||
$selector = $this->normalizeSelector($selector);
|
||||
$selectedItems = array_flip($this->getSelectedItems($driver, $selector));
|
||||
|
||||
$value = $this->getValueByPhrase($selectedItems, $name);
|
||||
$selected = $this->getSelectedItems($driver, $selector);
|
||||
$value = null;
|
||||
foreach ($selected as $val => $label) {
|
||||
$label = trim((string)$label);
|
||||
if ($label === $name || ($label !== '' && str_contains($label, $name))) { $value = (string)$val; break; }
|
||||
}
|
||||
if ($value === null) {
|
||||
throw new \InvalidArgumentException(sprintf('Could not find "%s" among selected items', $name));
|
||||
}
|
||||
|
||||
$this->removeItemByValue($driver, $selector, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wybierz pozycję po value (dokładny match).
|
||||
*/
|
||||
public function selectByValue(DriverInterface $driver, string $selector, string $value): void
|
||||
{
|
||||
$selector = $this->normalizeSelector($selector);
|
||||
$foundItems = $this->search($driver, $selector, $value);
|
||||
|
||||
if (!array_key_exists($value, $foundItems)) {
|
||||
$found = $this->search($driver, $selector, $value);
|
||||
if (!array_key_exists($value, $found)) {
|
||||
throw new \InvalidArgumentException(sprintf('Could not find "%s" in the autocomplete', $value));
|
||||
}
|
||||
|
||||
$this->addItemByValue($driver, $selector, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Usuń pozycję po value.
|
||||
*/
|
||||
public function removeByValue(DriverInterface $driver, string $selector, string $value): void
|
||||
{
|
||||
$selector = $this->normalizeSelector($selector);
|
||||
$selectedItems = $this->getSelectedItems($driver, $selector);
|
||||
|
||||
if (!array_key_exists($value, $selectedItems)) {
|
||||
throw new \InvalidArgumentException(sprintf('Could not find "%s" in the autocomplete selected items', $value));
|
||||
$selected = $this->getSelectedItems($driver, $selector);
|
||||
if (!array_key_exists($value, $selected)) {
|
||||
throw new \InvalidArgumentException(sprintf('Value "%s" is not selected', $value));
|
||||
}
|
||||
|
||||
$this->removeItemByValue($driver, $selector, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wyczyść wybór.
|
||||
*/
|
||||
public function clear(DriverInterface $driver, string $selector): void
|
||||
{
|
||||
$selector = $this->normalizeSelector($selector);
|
||||
$driver->executeScript(<<<SCRIPT
|
||||
$selector = $this->escapeForJsDoubleQuoted($selector);
|
||||
$driver->executeScript(<<<JS
|
||||
(function(){
|
||||
let element = document.evaluate("{$selector}", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
||||
element.tomselect.clear();
|
||||
element.tomselect.refreshOptions();
|
||||
var el = document.evaluate("{$selector}", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
||||
if (el && el.tomselect) { el.tomselect.clear(); el.tomselect.refreshOptions(false); }
|
||||
})();
|
||||
SCRIPT);
|
||||
JS);
|
||||
}
|
||||
|
||||
/* ======================== prywatne ======================== */
|
||||
|
||||
private function addItemByValue(DriverInterface $driver, string $selector, int|string $value): void
|
||||
{
|
||||
$driver->executeScript(<<<SCRIPT
|
||||
$selector = $this->escapeForJsDoubleQuoted($selector);
|
||||
$val = $this->toJsString((string)$value);
|
||||
|
||||
$driver->executeScript(<<<JS
|
||||
(function(){
|
||||
let element = document.evaluate("{$selector}", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
||||
element.tomselect.addItem('{$value}');
|
||||
element.tomselect.refreshOptions();
|
||||
var el = document.evaluate("{$selector}", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
||||
if (!el) return;
|
||||
var ts = el.tomselect;
|
||||
el.closest('.ts-wrapper')?.scrollIntoView({block:'center', inline:'center'});
|
||||
ts.addItem({$val});
|
||||
ts.refreshOptions(false);
|
||||
ts.close();
|
||||
})();
|
||||
SCRIPT);
|
||||
JS);
|
||||
}
|
||||
|
||||
private function removeItemByValue(DriverInterface $driver, string $selector, int|string $value): void
|
||||
{
|
||||
$driver->executeScript(<<<SCRIPT
|
||||
$selector = $this->escapeForJsDoubleQuoted($selector);
|
||||
$val = $this->toJsString((string)$value);
|
||||
|
||||
$driver->executeScript(<<<JS
|
||||
(function(){
|
||||
let element = document.evaluate("{$selector}", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
||||
element.tomselect.removeItem('{$value}');
|
||||
element.tomselect.refreshOptions();
|
||||
var el = document.evaluate("{$selector}", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
||||
if (!el) return;
|
||||
var ts = el.tomselect;
|
||||
ts.removeItem({$val});
|
||||
ts.refreshOptions(false);
|
||||
})();
|
||||
SCRIPT);
|
||||
JS);
|
||||
}
|
||||
|
||||
private function getValueByPhrase(array $foundItems, string $phrase): int|string
|
||||
/** Ucieczka do użycia wewnątrz podwójnych cudzysłowów w JS */
|
||||
private function escapeForJsDoubleQuoted(string $s): string
|
||||
{
|
||||
foreach ($foundItems as $foundName => $foundValue) {
|
||||
if (str_contains($foundName, $phrase)) {
|
||||
return $foundValue;
|
||||
}
|
||||
return str_replace('"', '\"', $s);
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException(sprintf('Could not find "%s" in the autocomplete', $phrase));
|
||||
}
|
||||
|
||||
private function normalizeSelector(string $selector): string
|
||||
/** Tworzy bezpieczny literał JS string */
|
||||
private function toJsString(string $s): string
|
||||
{
|
||||
return str_replace('"', '\'', $selector);
|
||||
return '"'.addslashes($s).'"';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ namespace Sylius\Bundle\ShopBundle\Controller;
|
|||
|
||||
use Sylius\Component\Channel\Context\ChannelContextInterface;
|
||||
use Sylius\Component\Core\Model\ChannelInterface;
|
||||
use Sylius\Component\Order\Context\CartContextInterface;
|
||||
use Sylius\Component\Order\Context\CartNotFoundException;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
|
@ -26,6 +28,7 @@ final class RegistrationThankYouController
|
|||
private Environment $twig,
|
||||
private ChannelContextInterface $channelContext,
|
||||
private RouterInterface $router,
|
||||
private CartContextInterface $cartContext,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -38,6 +41,16 @@ final class RegistrationThankYouController
|
|||
return new Response($this->twig->render('@SyliusShop/account/register_thank_you.html.twig'));
|
||||
}
|
||||
|
||||
try {
|
||||
$cart = $this->cartContext->getCart();
|
||||
|
||||
if (!$cart->getItems()->isEmpty()) {
|
||||
return new RedirectResponse($this->router->generate('sylius_shop_cart_summary'));
|
||||
}
|
||||
} catch (CartNotFoundException) {
|
||||
// No cart found, fall back to account dashboard
|
||||
}
|
||||
|
||||
return new RedirectResponse($this->router->generate('sylius_shop_account_dashboard'));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@
|
|||
<argument type="service" id="twig" />
|
||||
<argument type="service" id="sylius.context.channel" />
|
||||
<argument type="service" id="router.default" />
|
||||
<argument type="service" id="sylius.context.cart" />
|
||||
|
||||
<tag name="controller.service_arguments" />
|
||||
</service>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,9 @@
|
|||
{% import '@SyliusShop/shared/macro/navigation_buttons.html.twig' as navigation %}
|
||||
|
||||
{% set order = hookable_metadata.context.order %}
|
||||
{% set enabled = order.payments|length %}
|
||||
|
||||
{% for payment in hookable_metadata.context.form.payments %}
|
||||
{% set enabled = enabled and payment.method|length %}
|
||||
{% endfor %}
|
||||
|
||||
{% if sylius_is_shipping_required(order) %}
|
||||
{{ navigation.default('sylius.ui.change_shipping_method'|trans, path('sylius_shop_checkout_select_shipping'), enabled) }}
|
||||
{{ navigation.default('sylius.ui.change_shipping_method'|trans, path('sylius_shop_checkout_select_shipping'), true) }}
|
||||
{% else %}
|
||||
{{ navigation.default('sylius.ui.change_address'|trans, path('sylius_shop_checkout_address'), enabled) }}
|
||||
{{ navigation.default('sylius.ui.change_address'|trans, path('sylius_shop_checkout_address'), true) }}
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<div class="card bg-body-tertiary border-0 mb-3" {{ sylius_test_html_attribute('payment-item') }}>
|
||||
<label class="card-body">
|
||||
<label class="card-body d-flex flex-column gap-2" {{ sylius_test_html_attribute('payment-method-checkbox') }}>
|
||||
{% hook 'details' %}
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,20 @@
|
|||
{% set form = hookable_metadata.context.form %}
|
||||
{% set method = hookable_metadata.context.method %}
|
||||
{% set widget_attributes = sylius_test_form_attribute('payment-method-select') %}
|
||||
{% set widget_input_attr = widget_attributes.attr|default({}) %}
|
||||
{% set widget_input_attr = widget_input_attr|merge({
|
||||
'class': (widget_input_attr.class|default('') ~ ' form-check-input')|trim,
|
||||
}) %}
|
||||
{% set widget_attributes = widget_attributes|merge({
|
||||
'attr': widget_input_attr,
|
||||
'label_attr': {'class': 'visually-hidden'},
|
||||
}) %}
|
||||
|
||||
<div {{ sylius_test_html_attribute('payment-method-checkbox') }}>
|
||||
{{ form_widget(form, sylius_test_form_attribute('payment-method-select')) }}
|
||||
<div class="d-flex align-items-start gap-3">
|
||||
<div class="form-check mb-0">
|
||||
{{ form_widget(form, widget_attributes) }}
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<span class="fw-medium d-block">{{ method.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,4 @@
|
|||
{% import '@SyliusShop/shared/macro/navigation_buttons.html.twig' as navigation %}
|
||||
|
||||
{% set order = hookable_metadata.context.order %}
|
||||
{% set enabled = order.shipments|length %}
|
||||
|
||||
{% for shipment in hookable_metadata.context.form.shipments %}
|
||||
{% set enabled = enabled and shipment.method|length %}
|
||||
{% endfor %}
|
||||
|
||||
{{ navigation.default('sylius.ui.change_address'|trans, path('sylius_shop_checkout_address'), enabled) }}
|
||||
{{ navigation.default('sylius.ui.change_address'|trans, path('sylius_shop_checkout_address'), true) }}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
<div class="container sylius-messages">
|
||||
<div class="container sylius-messages" {{ sylius_test_html_attribute('sylius-flashes-container') }}>
|
||||
{% hook 'flashes' %}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue