diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 42ec099..800ed9d 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -72,3 +72,22 @@ jobs: dependencies: "${{ matrix.dependencies }}" symfony: "${{ matrix.symfony }}" testsuite: "functional" + + javascript-tests: + runs-on: "ubuntu-latest" + steps: + - uses: "actions/checkout@v4" + # The chooser JS unit tests live in tests/Application, whose package.json has file: deps on the + # installed Sylius/Symfony assets — so PHP + a composer install are needed to populate vendor/ + # before yarn can resolve them. + - uses: "shivammathur/setup-php@v2" + with: + php-version: "8.4" + extensions: "intl, mbstring" + coverage: "none" + - uses: "ramsey/composer-install@v4" + - uses: "actions/setup-node@v4" + with: + node-version: "22" + - run: "yarn --cwd tests/Application install" + - run: "yarn --cwd tests/Application test" diff --git a/CLAUDE.md b/CLAUDE.md index 7b6b9ed..5ddceb5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,7 +57,7 @@ When adding/changing a provider, the work happens in three places: the `Provider **Checkout pickup-point selection (async, no framework).** This is the heart of the shop UX and it deliberately makes **zero provider calls while the shipping page renders**, so a slow or down carrier can never stall checkout: - `Form/Extension/ShipmentTypeExtension` adds a hidden `pickupPoint` field (`Form/Type/PickupPointType`, a `HiddenType` + `Form/DataTransformer/PickupPointTransformer`). `Form/Extension/ShippingMethodChoiceTypeExtension` stamps `data-pickup-point-provider` onto each pickup-capable shipping-method radio. -- After the page is on screen, the framework-free `public/js/setono-pickup-point.js` (wired via the `_javascripts` twig hook on `sylius_shop.base#javascripts`) fetches `GET /pickup-points` (`Controller/Action/PickupPointsAction`) **once**. That endpoint returns each pickup-capable method's points for the current cart, keyed by method code, with a per-provider `try/catch` so one failing carrier doesn't take the others down. The script builds the radio list, toggles the visible group as the shipping method changes, and shows loading/empty/error states. It (re-)initialises on both `DOMContentLoaded` and `turbo:load` because Sylius' shop navigates with Turbo (checkout steps are AJAX body swaps). +- After the page is on screen, `public/js/setono-pickup-point.js` — a framework-free native **ES module** (` +``` + +## 3. React to the chooser — DOM events + +The chooser dispatches bubbling `CustomEvent`s on the field element (they bubble up to `document`): + +| Event | `detail` | Notes | +| --- | --- | --- | +| `setono:pickup-points:loaded` | `{ chooser, field, methodCode, points }` | points fetched for the selected method | +| `setono:pickup-points:error` | `{ chooser, field, error }` | the fetch failed | +| `setono:pickup-point:selected` | `{ chooser, field, point, token, source }` | a point was chosen (`source`: `auto` / `list` / `keep`); **cancelable** | + +```js +document.addEventListener('setono:pickup-point:selected', (event) => { + analytics.track('pickup_point_selected', event.detail.point); + // event.preventDefault() vetoes writing the token (e.g. to block auto-select) +}); +``` + +## 4. Change behaviour — subclass `PickupPointChooser` + +Register a subclass on `window.SetonoSyliusPickupPointChooser`; the boot instantiates it instead of the default. +Override one of the seams: `renderRow`, `renderSummary`, `renderList`, `formatAddress`, `shouldAutoSelect`, +`fetchPoints`. + +```html + +``` + +Because module scripts are deferred and `DOMContentLoaded` waits for them, your registration runs before the +chooser's boot reads the global — no ordering tricks needed. diff --git a/public/js/setono-pickup-point.js b/public/js/setono-pickup-point.js index 07a82c7..bb7c808 100644 --- a/public/js/setono-pickup-point.js +++ b/public/js/setono-pickup-point.js @@ -1,325 +1,462 @@ /** - * Pickup-point chooser (no framework, no Stimulus). + * Pickup-point chooser — a framework-free ES module (no build step, no Stimulus). * - * The shipping page renders without any provider calls. Once it is on screen this script fetches — once - * per visit, asynchronously — every pickup-capable method's points for the current cart from a single - * JSON endpoint (window.setonoSyliusPickupPointUrl); a slow or down provider therefore degrades - * gracefully instead of blocking checkout. + * The shipping page renders without any provider calls. Once it is on screen this module fetches — once per + * visit, asynchronously — every pickup-capable method's points for the current cart from a single JSON + * endpoint (window.setonoSyliusPickupPointUrl); a slow or down provider therefore degrades gracefully + * instead of blocking checkout. * - * The chooser is built client-side and inserted into the selected pickup-capable shipping method's card. - * It has two states: a compact summary of the chosen point (with a "Change" button) and an expandable - * list of points (with a "Selected" badge on the current one and a "Keep this" shortcut). Selecting a - * point writes its opaque token into the hidden input, which the server decodes on submit (no re-fetch). + * The chooser is built by CLONING server-rendered