@akinon/pz-theme 2.0.37-beta.0 → 2.0.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -4
- package/package.json +4 -4
- package/src/__tests__/__snapshots__/facet-filters-block.test.tsx.snap +11 -0
- package/src/__tests__/custom-css.test.ts +181 -0
- package/src/__tests__/designer-chrome-clipping.test.tsx +497 -0
- package/src/__tests__/facet-filters-block.test.tsx +707 -0
- package/src/__tests__/item-template.test.tsx +184 -0
- package/src/__tests__/listing-controls.test.tsx +961 -0
- package/src/__tests__/part-style-scope.test.tsx +229 -0
- package/src/__tests__/part-styles.test.ts +311 -0
- package/src/__tests__/registry-parity.test.ts +42 -0
- package/src/__tests__/row-visibility.test.ts +672 -0
- package/src/__tests__/sort-select-page-cursor.test.tsx +336 -0
- package/src/__tests__/theme-block-html-id.test.tsx +167 -0
- package/src/blocks/__tests__/__snapshots__/product-gallery-block.test.tsx.snap +15 -0
- package/src/blocks/__tests__/accordion-block-state.test.tsx +201 -0
- package/src/blocks/__tests__/facet-filters-block.test.tsx +190 -0
- package/src/blocks/__tests__/listing-context-card-model.test.tsx +230 -0
- package/src/blocks/__tests__/listing-lookup-hardening.test.ts +80 -0
- package/src/blocks/__tests__/product-gallery-block.test.tsx +974 -0
- package/src/blocks/__tests__/product-grid-composition.test.tsx +570 -0
- package/src/blocks/__tests__/product-grid-lookups.test.tsx +230 -0
- package/src/blocks/__tests__/product-price-block.test.tsx +248 -0
- package/src/blocks/__tests__/product-recommendations-block.test.tsx +702 -0
- package/src/blocks/__tests__/share-block.test.tsx +392 -0
- package/src/blocks/__tests__/store-locator-block.test.tsx +691 -0
- package/src/blocks/__tests__/variant-selector-block.test.tsx +817 -0
- package/src/blocks/__tests__/variant-selector-golden.json +20 -0
- package/src/blocks/account-menu-block.tsx +264 -0
- package/src/blocks/add-to-cart-block.tsx +417 -0
- package/src/blocks/address-list-block.tsx +348 -0
- package/src/blocks/applied-filters-block.tsx +288 -0
- package/src/blocks/auth-form-block.tsx +295 -0
- package/src/blocks/basket-items-block.tsx +457 -0
- package/src/blocks/basket-summary-block.tsx +404 -0
- package/src/blocks/block-renderer-registry.tsx +94 -0
- package/src/blocks/change-password-form-block.tsx +240 -0
- package/src/blocks/checkout-steps-block.tsx +288 -0
- package/src/blocks/checkout-summary-block.tsx +449 -0
- package/src/blocks/countdown-block.tsx +182 -0
- package/src/blocks/coupon-list-block.tsx +292 -0
- package/src/blocks/facet-filters-block.tsx +1686 -0
- package/src/blocks/faq-accordion-block.tsx +358 -0
- package/src/blocks/favorite-button-block.tsx +252 -0
- package/src/blocks/favourites-grid-block.tsx +510 -0
- package/src/blocks/forgot-password-block.tsx +155 -0
- package/src/blocks/item-template-utils.ts +131 -0
- package/src/blocks/item-template.tsx +164 -0
- package/src/blocks/listing-breadcrumb-block.tsx +175 -0
- package/src/blocks/listing-context.ts +850 -0
- package/src/blocks/listing-pagination-block.tsx +923 -0
- package/src/blocks/listing-result-count-block.tsx +126 -0
- package/src/blocks/listing-title-block.tsx +115 -0
- package/src/blocks/oauth-buttons-block.tsx +205 -0
- package/src/blocks/order-detail-block.tsx +474 -0
- package/src/blocks/order-list-block.tsx +274 -0
- package/src/blocks/order-success-block.tsx +641 -0
- package/src/blocks/order-tracking-block.tsx +533 -0
- package/src/blocks/product-attributes-block.tsx +307 -0
- package/src/blocks/product-block-utils.tsx +639 -0
- package/src/blocks/product-breadcrumb-block.tsx +215 -0
- package/src/blocks/product-description-block.tsx +169 -0
- package/src/blocks/product-gallery-block.tsx +1523 -0
- package/src/blocks/product-grid-block.tsx +966 -0
- package/src/blocks/product-price-block.tsx +373 -0
- package/src/blocks/product-recommendations-block.tsx +1433 -0
- package/src/blocks/product-title-block.tsx +129 -0
- package/src/blocks/profile-form-block.tsx +297 -0
- package/src/blocks/recently-viewed-store.ts +201 -0
- package/src/blocks/register-form-block.tsx +362 -0
- package/src/blocks/share-block.tsx +667 -0
- package/src/blocks/size-guide-block.tsx +313 -0
- package/src/blocks/sort-select-block.tsx +636 -0
- package/src/blocks/stock-status-block.tsx +168 -0
- package/src/blocks/store-locator-block.tsx +1184 -0
- package/src/blocks/variant-selector-block.tsx +1110 -0
- package/src/sections/__tests__/tablet-raw-bucket-readers.test.tsx +186 -0
- package/src/utils/__tests__/part-ownership-guard.test.tsx +29 -4
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { act, fireEvent, render, waitFor } from '@testing-library/react';
|
|
3
|
+
import StoreLocatorBlock from '../store-locator-block';
|
|
4
|
+
import { ThemePageContextProvider } from '../../theme-page-context';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The store list is the block a brand already has on the page, and the find-in-
|
|
8
|
+
* store form is new capability bolted beside it. Two things have to hold: the
|
|
9
|
+
* list a published composition renders today cannot move by a byte, and the
|
|
10
|
+
* form that opens over the page has to be a real dialog rather than a div that
|
|
11
|
+
* looks like one.
|
|
12
|
+
*
|
|
13
|
+
* The golden below was captured by rendering the block at the commit BEFORE
|
|
14
|
+
* this work and dumping `container.innerHTML`; it is pasted here verbatim, so
|
|
15
|
+
* it cannot be regenerated from the code it is meant to police.
|
|
16
|
+
*/
|
|
17
|
+
const GOLDEN: Record<string, string> = {
|
|
18
|
+
"default":
|
|
19
|
+
"<div data-store-locator=\"\" style=\"display: flex; flex-direction: column; gap: var(--store-locator-gap, 16px);\"><label data-store-locator-filter=\"\" data-part=\"filter\" style=\"display: inline-flex; align-items: center; gap: 8px;\"><span data-part=\"filter-label\">City</span><select data-part=\"select\" style=\"background: transparent; padding: 8px 10px;\"><option value=\"\">All cities</option><option value=\"Ankara\">Ankara</option><option value=\"İstanbul\">İstanbul</option><option value=\"İzmir\">İzmir</option></select></label><ul data-part=\"list\" style=\"display: flex; flex-direction: column; gap: var(--store-locator-gap, 16px); list-style: none; margin: 0px; padding: 0px;\"><li data-store-locator-item=\"\" data-part=\"card\" style=\"display: flex; flex-direction: column; gap: 4px; padding-bottom: 12px;\"><span data-store-locator-name=\"\" data-part=\"name\" style=\"font-weight: 600;\">Kanyon AVM</span><span data-store-locator-address=\"\" data-part=\"address\" style=\"opacity: 0.8;\">Büyükdere Cad. No:185</span><span data-store-locator-city=\"\" data-part=\"city\" style=\"opacity: 0.7;\">İstanbul</span><a data-store-locator-phone=\"\" data-part=\"phone\" href=\"tel:+902123531500\">+902123531500</a><a data-store-locator-map=\"\" data-part=\"map-link\" href=\"https://www.google.com/maps/search/?api=1&query=41.0678,29.0092\" target=\"_blank\" rel=\"noopener noreferrer\">View on map</a></li><li data-store-locator-item=\"\" data-part=\"card\" style=\"display: flex; flex-direction: column; gap: 4px; padding-bottom: 12px;\"><span data-store-locator-name=\"\" data-part=\"name\" style=\"font-weight: 600;\">Ankamall</span><span data-store-locator-address=\"\" data-part=\"address\" style=\"opacity: 0.8;\">Akköprü Mah.</span><span data-store-locator-city=\"\" data-part=\"city\" style=\"opacity: 0.7;\">Ankara</span><a data-store-locator-phone=\"\" data-part=\"phone\" href=\"tel:+903125411000\">+903125411000</a><a data-store-locator-map=\"\" data-part=\"map-link\" href=\"https://www.google.com/maps/search/?api=1&query=39.9497,32.8241\" target=\"_blank\" rel=\"noopener noreferrer\">View on map</a></li><li data-store-locator-item=\"\" data-part=\"card\" style=\"display: flex; flex-direction: column; gap: 4px; padding-bottom: 12px;\"><span data-store-locator-name=\"\" data-part=\"name\" style=\"font-weight: 600;\">Merkez</span><span data-store-locator-city=\"\" data-part=\"city\" style=\"opacity: 0.7;\">İzmir</span></li></ul></div>",
|
|
20
|
+
"default-designer":
|
|
21
|
+
"<div data-store-locator=\"\" style=\"display: flex; flex-direction: column; gap: var(--store-locator-gap, 16px);\"><label data-store-locator-filter=\"\" data-part=\"filter\" style=\"display: inline-flex; align-items: center; gap: 8px;\"><span data-part=\"filter-label\">City</span><select data-part=\"select\" style=\"background: transparent; padding: 8px 10px;\"><option value=\"\">All cities</option><option value=\"Ankara\">Ankara</option><option value=\"İstanbul\">İstanbul</option><option value=\"İzmir\">İzmir</option></select></label><ul data-part=\"list\" style=\"display: flex; flex-direction: column; gap: var(--store-locator-gap, 16px); list-style: none; margin: 0px; padding: 0px;\"><li data-store-locator-item=\"\" data-part=\"card\" style=\"display: flex; flex-direction: column; gap: 4px; padding-bottom: 12px;\"><span data-store-locator-name=\"\" data-part=\"name\" style=\"font-weight: 600;\">Kanyon AVM</span><span data-store-locator-address=\"\" data-part=\"address\" style=\"opacity: 0.8;\">Büyükdere Cad. No:185</span><span data-store-locator-city=\"\" data-part=\"city\" style=\"opacity: 0.7;\">İstanbul</span><a data-store-locator-phone=\"\" data-part=\"phone\" href=\"#\">+902123531500</a><a data-store-locator-map=\"\" data-part=\"map-link\" href=\"#\" rel=\"noopener noreferrer\">View on map</a></li><li data-store-locator-item=\"\" data-part=\"card\" style=\"display: flex; flex-direction: column; gap: 4px; padding-bottom: 12px;\"><span data-store-locator-name=\"\" data-part=\"name\" style=\"font-weight: 600;\">Ankamall</span><span data-store-locator-address=\"\" data-part=\"address\" style=\"opacity: 0.8;\">Akköprü Mah.</span><span data-store-locator-city=\"\" data-part=\"city\" style=\"opacity: 0.7;\">Ankara</span><a data-store-locator-phone=\"\" data-part=\"phone\" href=\"#\">+903125411000</a><a data-store-locator-map=\"\" data-part=\"map-link\" href=\"#\" rel=\"noopener noreferrer\">View on map</a></li><li data-store-locator-item=\"\" data-part=\"card\" style=\"display: flex; flex-direction: column; gap: 4px; padding-bottom: 12px;\"><span data-store-locator-name=\"\" data-part=\"name\" style=\"font-weight: 600;\">Merkez</span><span data-store-locator-city=\"\" data-part=\"city\" style=\"opacity: 0.7;\">İzmir</span></li></ul></div>",
|
|
22
|
+
"grid":
|
|
23
|
+
"<div data-store-locator=\"\" style=\"display: flex; flex-direction: column; gap: var(--store-locator-gap, 16px);\"><label data-store-locator-filter=\"\" data-part=\"filter\" style=\"display: inline-flex; align-items: center; gap: 8px;\"><span data-part=\"filter-label\">City</span><select data-part=\"select\" style=\"background: transparent; padding: 8px 10px;\"><option value=\"\">All cities</option><option value=\"Ankara\">Ankara</option><option value=\"İstanbul\">İstanbul</option><option value=\"İzmir\">İzmir</option></select></label><style>[data-store-locator-id=\"sl-1\"]{grid-template-columns:repeat(2,minmax(0,1fr));}@media (max-width:1023px){[data-store-locator-id=\"sl-1\"]{grid-template-columns:repeat(2,minmax(0,1fr));}}</style><ul data-part=\"list\" data-store-locator-id=\"sl-1\" style=\"display: grid; gap: var(--store-locator-gap, 16px); list-style: none; margin: 0px; padding: 0px;\"><li data-store-locator-item=\"\" data-part=\"card\" style=\"display: flex; flex-direction: column; gap: 4px; padding-bottom: 12px;\"><span data-store-locator-name=\"\" data-part=\"name\" style=\"font-weight: 600;\">Ankamall</span><span data-store-locator-address=\"\" data-part=\"address\" style=\"opacity: 0.8;\">Akköprü Mah.</span><span data-store-locator-city=\"\" data-part=\"city\" style=\"opacity: 0.7;\">Ankara</span><a data-store-locator-phone=\"\" data-part=\"phone\" href=\"tel:+903125411000\">+903125411000</a><a data-store-locator-map=\"\" data-part=\"map-link\" href=\"https://www.google.com/maps/search/?api=1&query=39.9497,32.8241\" target=\"_blank\" rel=\"noopener noreferrer\">View on map</a></li><li data-store-locator-item=\"\" data-part=\"card\" style=\"display: flex; flex-direction: column; gap: 4px; padding-bottom: 12px;\"><span data-store-locator-name=\"\" data-part=\"name\" style=\"font-weight: 600;\">Kanyon AVM</span><span data-store-locator-address=\"\" data-part=\"address\" style=\"opacity: 0.8;\">Büyükdere Cad. No:185</span><span data-store-locator-city=\"\" data-part=\"city\" style=\"opacity: 0.7;\">İstanbul</span><a data-store-locator-phone=\"\" data-part=\"phone\" href=\"tel:+902123531500\">+902123531500</a><a data-store-locator-map=\"\" data-part=\"map-link\" href=\"https://www.google.com/maps/search/?api=1&query=41.0678,29.0092\" target=\"_blank\" rel=\"noopener noreferrer\">View on map</a></li><li data-store-locator-item=\"\" data-part=\"card\" style=\"display: flex; flex-direction: column; gap: 4px; padding-bottom: 12px;\"><span data-store-locator-name=\"\" data-part=\"name\" style=\"font-weight: 600;\">Merkez</span><span data-store-locator-city=\"\" data-part=\"city\" style=\"opacity: 0.7;\">İzmir</span></li></ul></div>",
|
|
24
|
+
"no-filter":
|
|
25
|
+
"<div data-store-locator=\"\" style=\"display: flex; flex-direction: column; gap: var(--store-locator-gap, 16px);\"><ul data-part=\"list\" style=\"display: flex; flex-direction: column; gap: var(--store-locator-gap, 16px); list-style: none; margin: 0px; padding: 0px;\"><li data-store-locator-item=\"\" data-part=\"card\" style=\"display: flex; flex-direction: column; gap: 4px; padding-bottom: 12px;\"><span data-store-locator-name=\"\" data-part=\"name\" style=\"font-weight: 600;\">Kanyon AVM</span><span data-store-locator-address=\"\" data-part=\"address\" style=\"opacity: 0.8;\">Büyükdere Cad. No:185</span><span data-store-locator-city=\"\" data-part=\"city\" style=\"opacity: 0.7;\">İstanbul</span></li><li data-store-locator-item=\"\" data-part=\"card\" style=\"display: flex; flex-direction: column; gap: 4px; padding-bottom: 12px;\"><span data-store-locator-name=\"\" data-part=\"name\" style=\"font-weight: 600;\">Ankamall</span><span data-store-locator-address=\"\" data-part=\"address\" style=\"opacity: 0.8;\">Akköprü Mah.</span><span data-store-locator-city=\"\" data-part=\"city\" style=\"opacity: 0.7;\">Ankara</span></li><li data-store-locator-item=\"\" data-part=\"card\" style=\"display: flex; flex-direction: column; gap: 4px; padding-bottom: 12px;\"><span data-store-locator-name=\"\" data-part=\"name\" style=\"font-weight: 600;\">Merkez</span><span data-store-locator-city=\"\" data-part=\"city\" style=\"opacity: 0.7;\">İzmir</span></li></ul></div>",
|
|
26
|
+
"distance":
|
|
27
|
+
"<div data-store-locator=\"\" style=\"display: flex; flex-direction: column; gap: var(--store-locator-gap, 16px);\"><label data-store-locator-filter=\"\" data-part=\"filter\" style=\"display: inline-flex; align-items: center; gap: 8px;\"><span data-part=\"filter-label\">City</span><select data-part=\"select\" style=\"background: transparent; padding: 8px 10px;\"><option value=\"\">All cities</option><option value=\"Ankara\">Ankara</option><option value=\"İstanbul\">İstanbul</option><option value=\"İzmir\">İzmir</option></select></label><button type=\"button\" data-store-locator-location=\"\" data-part=\"location-button\" style=\"display: inline-flex; align-items: center; gap: 8px; padding: 8px 12px; background: transparent; cursor: pointer;\">Use my location</button><ul data-part=\"list\" style=\"display: flex; flex-direction: column; gap: var(--store-locator-gap, 16px); list-style: none; margin: 0px; padding: 0px;\"><li data-store-locator-item=\"\" data-part=\"card\" style=\"display: flex; flex-direction: column; gap: 4px; padding-bottom: 12px;\"><span data-store-locator-name=\"\" data-part=\"name\" style=\"font-weight: 600;\">Kanyon AVM</span><span data-store-locator-address=\"\" data-part=\"address\" style=\"opacity: 0.8;\">Büyükdere Cad. No:185</span><span data-store-locator-city=\"\" data-part=\"city\" style=\"opacity: 0.7;\">İstanbul</span><a data-store-locator-phone=\"\" data-part=\"phone\" href=\"tel:+902123531500\">+902123531500</a><a data-store-locator-map=\"\" data-part=\"map-link\" href=\"https://www.google.com/maps/search/?api=1&query=41.0678,29.0092\" target=\"_blank\" rel=\"noopener noreferrer\">View on map</a></li><li data-store-locator-item=\"\" data-part=\"card\" style=\"display: flex; flex-direction: column; gap: 4px; padding-bottom: 12px;\"><span data-store-locator-name=\"\" data-part=\"name\" style=\"font-weight: 600;\">Ankamall</span><span data-store-locator-address=\"\" data-part=\"address\" style=\"opacity: 0.8;\">Akköprü Mah.</span><span data-store-locator-city=\"\" data-part=\"city\" style=\"opacity: 0.7;\">Ankara</span><a data-store-locator-phone=\"\" data-part=\"phone\" href=\"tel:+903125411000\">+903125411000</a><a data-store-locator-map=\"\" data-part=\"map-link\" href=\"https://www.google.com/maps/search/?api=1&query=39.9497,32.8241\" target=\"_blank\" rel=\"noopener noreferrer\">View on map</a></li><li data-store-locator-item=\"\" data-part=\"card\" style=\"display: flex; flex-direction: column; gap: 4px; padding-bottom: 12px;\"><span data-store-locator-name=\"\" data-part=\"name\" style=\"font-weight: 600;\">Merkez</span><span data-store-locator-city=\"\" data-part=\"city\" style=\"opacity: 0.7;\">İzmir</span></li></ul></div>",
|
|
28
|
+
// The trigger form, captured the same way at the same commit. It is newer
|
|
29
|
+
// than the list above and unreleased, but "unreleased" is exactly when a
|
|
30
|
+
// golden is worth having: `aria-haspopup` on the trigger, `aria-hidden` on
|
|
31
|
+
// the backdrop, `role`/`aria-modal`/`aria-label`/`tabindex` on the panel and
|
|
32
|
+
// the placement box are all load-bearing, and none of them is obvious enough
|
|
33
|
+
// to survive a refactor by luck.
|
|
34
|
+
"trigger":
|
|
35
|
+
"<div data-store-locator=\"\" data-store-locator-mode=\"trigger\" style=\"position: relative;\"><button type=\"button\" data-store-locator-trigger=\"\" data-part=\"trigger\" aria-haspopup=\"dialog\" aria-expanded=\"false\" style=\"display: inline-flex; align-items: center; gap: 8px; padding: 10px 16px; background: transparent; cursor: pointer;\">Find in store</button></div>",
|
|
36
|
+
"trigger-open":
|
|
37
|
+
"<div data-store-locator=\"\" data-store-locator-mode=\"trigger\" style=\"position: relative;\"><button type=\"button\" data-store-locator-trigger=\"\" data-part=\"trigger\" aria-haspopup=\"dialog\" aria-expanded=\"true\" style=\"display: inline-flex; align-items: center; gap: 8px; padding: 10px 16px; background: transparent; cursor: pointer;\" data-state=\"open\">Find in store</button><div data-store-locator-backdrop=\"\" data-part=\"backdrop\" aria-hidden=\"true\" style=\"position: fixed; inset: 0; z-index: 40;\"></div><div data-store-locator-panel=\"\" data-part=\"panel\" data-state=\"open\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Find in store\" tabindex=\"-1\" style=\"position: fixed; z-index: 41; display: flex; flex-direction: column; gap: var(--store-locator-gap, 16px); padding: 20px; overflow-y: auto; top: 50%; left: 50%; transform: translate(-50%, -50%); max-height: calc(100% - 64px);\"><div style=\"display: flex; align-items: center; justify-content: space-between; gap: 12px;\"><span data-store-locator-panel-title=\"\" data-part=\"panel-title\" style=\"font-weight: 600;\">Find in store</span><button type=\"button\" data-store-locator-panel-close=\"\" data-part=\"panel-close\" style=\"background: transparent; cursor: pointer; padding: 4px;\">Close</button></div><div style=\"display: flex; flex-direction: column; gap: var(--store-locator-gap, 16px);\"><label data-store-locator-filter=\"\" data-part=\"filter\" style=\"display: inline-flex; align-items: center; gap: 8px;\"><span data-part=\"filter-label\">City</span><select data-part=\"select\" style=\"background: transparent; padding: 8px 10px;\"><option value=\"\">All cities</option><option value=\"Ankara\">Ankara</option><option value=\"İstanbul\">İstanbul</option><option value=\"İzmir\">İzmir</option></select></label><ul data-part=\"list\" style=\"display: flex; flex-direction: column; gap: var(--store-locator-gap, 16px); list-style: none; margin: 0px; padding: 0px;\"><li data-store-locator-item=\"\" data-part=\"card\" style=\"display: flex; flex-direction: column; gap: 4px; padding-bottom: 12px;\"><span data-store-locator-name=\"\" data-part=\"name\" style=\"font-weight: 600;\">Kanyon AVM</span><span data-store-locator-address=\"\" data-part=\"address\" style=\"opacity: 0.8;\">Büyükdere Cad. No:185</span><span data-store-locator-city=\"\" data-part=\"city\" style=\"opacity: 0.7;\">İstanbul</span><a data-store-locator-phone=\"\" data-part=\"phone\" href=\"tel:+902123531500\">+902123531500</a><a data-store-locator-map=\"\" data-part=\"map-link\" href=\"https://www.google.com/maps/search/?api=1&query=41.0678,29.0092\" target=\"_blank\" rel=\"noopener noreferrer\">View on map</a></li><li data-store-locator-item=\"\" data-part=\"card\" style=\"display: flex; flex-direction: column; gap: 4px; padding-bottom: 12px;\"><span data-store-locator-name=\"\" data-part=\"name\" style=\"font-weight: 600;\">Ankamall</span><span data-store-locator-address=\"\" data-part=\"address\" style=\"opacity: 0.8;\">Akköprü Mah.</span><span data-store-locator-city=\"\" data-part=\"city\" style=\"opacity: 0.7;\">Ankara</span><a data-store-locator-phone=\"\" data-part=\"phone\" href=\"tel:+903125411000\">+903125411000</a><a data-store-locator-map=\"\" data-part=\"map-link\" href=\"https://www.google.com/maps/search/?api=1&query=39.9497,32.8241\" target=\"_blank\" rel=\"noopener noreferrer\">View on map</a></li><li data-store-locator-item=\"\" data-part=\"card\" style=\"display: flex; flex-direction: column; gap: 4px; padding-bottom: 12px;\"><span data-store-locator-name=\"\" data-part=\"name\" style=\"font-weight: 600;\">Merkez</span><span data-store-locator-city=\"\" data-part=\"city\" style=\"opacity: 0.7;\">İzmir</span></li></ul></div></div></div>",
|
|
38
|
+
"trigger-open-designer":
|
|
39
|
+
"<div data-store-locator=\"\" data-store-locator-mode=\"trigger\" style=\"position: relative;\"><button type=\"button\" data-store-locator-trigger=\"\" data-part=\"trigger\" data-state=\"open\" aria-haspopup=\"dialog\" aria-expanded=\"true\" style=\"display: inline-flex; align-items: center; gap: 8px; padding: 10px 16px; background: transparent; cursor: pointer;\">Find in store</button><div data-store-locator-backdrop=\"\" data-part=\"backdrop\" aria-hidden=\"true\" style=\"position: fixed; inset: 0; z-index: 40;\"></div><div data-store-locator-panel=\"\" data-part=\"panel\" data-state=\"open\" role=\"dialog\" aria-label=\"Find in store\" tabindex=\"-1\" style=\"position: fixed; z-index: 41; display: flex; flex-direction: column; gap: var(--store-locator-gap, 16px); padding: 20px; overflow-y: auto; top: 0px; right: 0px; bottom: 0px;\"><div style=\"display: flex; align-items: center; justify-content: space-between; gap: 12px;\"><span data-store-locator-panel-title=\"\" data-part=\"panel-title\" style=\"font-weight: 600;\">Find in store</span><button type=\"button\" data-store-locator-panel-close=\"\" data-part=\"panel-close\" style=\"background: transparent; cursor: pointer; padding: 4px;\">Close</button></div><div style=\"display: flex; flex-direction: column; gap: var(--store-locator-gap, 16px);\"><label data-store-locator-filter=\"\" data-part=\"filter\" style=\"display: inline-flex; align-items: center; gap: 8px;\"><span data-part=\"filter-label\">City</span><select data-part=\"select\" style=\"background: transparent; padding: 8px 10px;\"><option value=\"\">All cities</option><option value=\"Ankara\">Ankara</option><option value=\"İstanbul\">İstanbul</option><option value=\"İzmir\">İzmir</option></select></label><ul data-part=\"list\" style=\"display: flex; flex-direction: column; gap: var(--store-locator-gap, 16px); list-style: none; margin: 0px; padding: 0px;\"><li data-store-locator-item=\"\" data-part=\"card\" style=\"display: flex; flex-direction: column; gap: 4px; padding-bottom: 12px;\"><span data-store-locator-name=\"\" data-part=\"name\" style=\"font-weight: 600;\">Kanyon AVM</span><span data-store-locator-address=\"\" data-part=\"address\" style=\"opacity: 0.8;\">Büyükdere Cad. No:185</span><span data-store-locator-city=\"\" data-part=\"city\" style=\"opacity: 0.7;\">İstanbul</span><a data-store-locator-phone=\"\" data-part=\"phone\" href=\"#\">+902123531500</a><a data-store-locator-map=\"\" data-part=\"map-link\" href=\"#\" rel=\"noopener noreferrer\">View on map</a></li><li data-store-locator-item=\"\" data-part=\"card\" style=\"display: flex; flex-direction: column; gap: 4px; padding-bottom: 12px;\"><span data-store-locator-name=\"\" data-part=\"name\" style=\"font-weight: 600;\">Ankamall</span><span data-store-locator-address=\"\" data-part=\"address\" style=\"opacity: 0.8;\">Akköprü Mah.</span><span data-store-locator-city=\"\" data-part=\"city\" style=\"opacity: 0.7;\">Ankara</span><a data-store-locator-phone=\"\" data-part=\"phone\" href=\"#\">+903125411000</a><a data-store-locator-map=\"\" data-part=\"map-link\" href=\"#\" rel=\"noopener noreferrer\">View on map</a></li><li data-store-locator-item=\"\" data-part=\"card\" style=\"display: flex; flex-direction: column; gap: 4px; padding-bottom: 12px;\"><span data-store-locator-name=\"\" data-part=\"name\" style=\"font-weight: 600;\">Merkez</span><span data-store-locator-city=\"\" data-part=\"city\" style=\"opacity: 0.7;\">İzmir</span></li></ul></div></div></div>"
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* `innerHTML` records the order attributes were ADDED in rather than the
|
|
44
|
+
* markup: one that first appears on a later commit is appended at the end. The
|
|
45
|
+
* trigger goldens are therefore compared attribute-set by attribute-set — every
|
|
46
|
+
* element, every attribute and every value still has to match exactly, but a
|
|
47
|
+
* value that arrives one commit later is not a golden break.
|
|
48
|
+
*/
|
|
49
|
+
const canonical = (html: string): string => {
|
|
50
|
+
const host = document.createElement('div');
|
|
51
|
+
host.innerHTML = html;
|
|
52
|
+
|
|
53
|
+
const sortAttributes = (element: Element) => {
|
|
54
|
+
const pairs = element
|
|
55
|
+
.getAttributeNames()
|
|
56
|
+
.sort()
|
|
57
|
+
.map((name) => [name, element.getAttribute(name) ?? ''] as const);
|
|
58
|
+
|
|
59
|
+
pairs.forEach(([name]) => element.removeAttribute(name));
|
|
60
|
+
pairs.forEach(([name, value]) => element.setAttribute(name, value));
|
|
61
|
+
Array.from(element.children).forEach(sortAttributes);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
Array.from(host.children).forEach(sortAttributes);
|
|
65
|
+
|
|
66
|
+
return host.innerHTML;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** The store rows the golden was captured against — they may not be edited. */
|
|
70
|
+
const GOLDEN_STORES = [
|
|
71
|
+
{
|
|
72
|
+
pk: 1,
|
|
73
|
+
name: 'Kanyon AVM',
|
|
74
|
+
address: 'Büyükdere Cad. No:185',
|
|
75
|
+
phone_number: '+902123531500',
|
|
76
|
+
latitude: '41.0678',
|
|
77
|
+
longitude: '29.0092',
|
|
78
|
+
township: { name: 'Şişli', city: { name: 'İstanbul' } }
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
pk: 2,
|
|
82
|
+
name: 'Ankamall',
|
|
83
|
+
address: 'Akköprü Mah.',
|
|
84
|
+
phone_number: '+903125411000',
|
|
85
|
+
latitude: '39.9497',
|
|
86
|
+
longitude: '32.8241',
|
|
87
|
+
city: { name: 'Ankara' }
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
pk: 3,
|
|
91
|
+
name: 'Merkez',
|
|
92
|
+
address: '',
|
|
93
|
+
phone_number: '',
|
|
94
|
+
latitude: '0',
|
|
95
|
+
longitude: '0',
|
|
96
|
+
city: 'İzmir'
|
|
97
|
+
}
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
let mockStores: unknown[] = [];
|
|
101
|
+
let mockCities: unknown[] = [];
|
|
102
|
+
let mockStockRows: unknown[] = [];
|
|
103
|
+
let mockStockRejects = false;
|
|
104
|
+
const stockCalls: unknown[] = [];
|
|
105
|
+
|
|
106
|
+
jest.mock('@akinon/next/data/client/address', () => ({
|
|
107
|
+
__esModule: true,
|
|
108
|
+
useGetStoresQuery: () => ({
|
|
109
|
+
data: { results: mockStores },
|
|
110
|
+
isLoading: false
|
|
111
|
+
}),
|
|
112
|
+
useGetRetailStoreQuery: () => ({ data: { results: mockCities } }),
|
|
113
|
+
useGetRetailStoreStockMutation: () => [
|
|
114
|
+
(args: unknown) => {
|
|
115
|
+
stockCalls.push(args);
|
|
116
|
+
return {
|
|
117
|
+
unwrap: () =>
|
|
118
|
+
mockStockRejects
|
|
119
|
+
? Promise.reject(new Error('commerce is down'))
|
|
120
|
+
: Promise.resolve(mockStockRows)
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
]
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
const PRODUCT_CONTEXT = {
|
|
127
|
+
product: {
|
|
128
|
+
pk: 4242,
|
|
129
|
+
name: 'Oxford Ayakkabı',
|
|
130
|
+
variants: [
|
|
131
|
+
{
|
|
132
|
+
attribute_key: 'size',
|
|
133
|
+
attribute_name: 'Beden',
|
|
134
|
+
options: [{ value: '42', is_selected: true }]
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const renderBlock = (
|
|
141
|
+
properties: Record<string, unknown>,
|
|
142
|
+
extra: Record<string, unknown> = {},
|
|
143
|
+
context: Record<string, unknown> | null = null
|
|
144
|
+
) =>
|
|
145
|
+
render(
|
|
146
|
+
<ThemePageContextProvider value={context}>
|
|
147
|
+
<StoreLocatorBlock
|
|
148
|
+
{...({
|
|
149
|
+
block: {
|
|
150
|
+
id: 'sl-1',
|
|
151
|
+
type: 'store-locator',
|
|
152
|
+
styles: {},
|
|
153
|
+
properties,
|
|
154
|
+
order: 0,
|
|
155
|
+
hidden: false
|
|
156
|
+
},
|
|
157
|
+
placeholderId: 'p',
|
|
158
|
+
sectionId: 's',
|
|
159
|
+
...extra
|
|
160
|
+
} as never)}
|
|
161
|
+
/>
|
|
162
|
+
</ThemePageContextProvider>
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const trigger = (c: HTMLElement) =>
|
|
166
|
+
c.querySelector('[data-store-locator-trigger]') as HTMLButtonElement;
|
|
167
|
+
const panel = (c: HTMLElement) =>
|
|
168
|
+
c.querySelector('[data-store-locator-panel]') as HTMLElement | null;
|
|
169
|
+
|
|
170
|
+
beforeEach(() => {
|
|
171
|
+
mockStores = GOLDEN_STORES;
|
|
172
|
+
mockCities = [];
|
|
173
|
+
mockStockRows = [];
|
|
174
|
+
mockStockRejects = false;
|
|
175
|
+
stockCalls.length = 0;
|
|
176
|
+
document.body.style.overflow = '';
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('store locator — the list that already ships', () => {
|
|
180
|
+
it('renders the default composition byte for byte', () => {
|
|
181
|
+
expect(renderBlock({}).container.innerHTML).toBe(GOLDEN['default']);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('renders the default composition in the designer byte for byte', () => {
|
|
185
|
+
expect(renderBlock({}, { isDesigner: true }).container.innerHTML).toBe(
|
|
186
|
+
GOLDEN['default-designer']
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('renders a sorted grid byte for byte', () => {
|
|
191
|
+
expect(
|
|
192
|
+
renderBlock({ columns: 2, columnsMobile: 2, sort: 'name' }).container
|
|
193
|
+
.innerHTML
|
|
194
|
+
).toBe(GOLDEN['grid']);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('renders a stripped-down list byte for byte', () => {
|
|
198
|
+
expect(
|
|
199
|
+
renderBlock({ showCityFilter: false, showMap: false, showPhone: false })
|
|
200
|
+
.container.innerHTML
|
|
201
|
+
).toBe(GOLDEN['no-filter']);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('renders the distance form byte for byte', () => {
|
|
205
|
+
expect(renderBlock({ showDistance: true }).container.innerHTML).toBe(
|
|
206
|
+
GOLDEN['distance']
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('adds nothing to the availability list but the notice route', () => {
|
|
211
|
+
// The one place the new work reaches an existing render, and it is a
|
|
212
|
+
// composition that had to opt into `productMode` to get there. Removing
|
|
213
|
+
// the added anchor has to leave the previous markup exactly.
|
|
214
|
+
const html = renderBlock(
|
|
215
|
+
{ productMode: true, availabilityHelpUrl: 'https://brand.example/stores' },
|
|
216
|
+
{},
|
|
217
|
+
PRODUCT_CONTEXT
|
|
218
|
+
).container.innerHTML;
|
|
219
|
+
|
|
220
|
+
const added =
|
|
221
|
+
'<a data-store-locator-notice-link="" data-part="notice-link" ' +
|
|
222
|
+
'href="https://brand.example/stores" style="font-size: 0.9em;">' +
|
|
223
|
+
'Check availability</a>';
|
|
224
|
+
|
|
225
|
+
expect(html).toContain(added);
|
|
226
|
+
expect(html.replace(added, '')).toBe(
|
|
227
|
+
"<div data-store-locator=\"\" style=\"display: flex; flex-direction: column; gap: var(--store-locator-gap, 16px);\"><label data-store-locator-filter=\"\" data-part=\"filter\" style=\"display: inline-flex; align-items: center; gap: 8px;\"><span data-part=\"filter-label\">City</span><select data-part=\"select\" style=\"background: transparent; padding: 8px 10px;\"><option value=\"\">All cities</option><option value=\"Ankara\">Ankara</option><option value=\"İstanbul\">İstanbul</option><option value=\"İzmir\">İzmir</option></select></label><span data-store-locator-notice=\"\" data-part=\"notice\" style=\"font-size: 0.9em; opacity: 0.8;\">Pick a city to see stock for this product</span><ul data-part=\"list\" style=\"display: flex; flex-direction: column; gap: var(--store-locator-gap, 16px); list-style: none; margin: 0px; padding: 0px;\"><li data-store-locator-item=\"\" data-part=\"card\" style=\"display: flex; flex-direction: column; gap: 4px; padding-bottom: 12px;\"><span data-store-locator-name=\"\" data-part=\"name\" style=\"font-weight: 600;\">Kanyon AVM</span><a data-store-locator-availability=\"\" data-store-locator-availability-state=\"unknown\" data-part=\"availability\" data-state=\"unknown\" href=\"https://brand.example/stores\" style=\"font-size: 0.85em;\">Check availability</a><span data-store-locator-address=\"\" data-part=\"address\" style=\"opacity: 0.8;\">Büyükdere Cad. No:185</span><span data-store-locator-city=\"\" data-part=\"city\" style=\"opacity: 0.7;\">İstanbul</span><a data-store-locator-phone=\"\" data-part=\"phone\" href=\"tel:+902123531500\">+902123531500</a><a data-store-locator-map=\"\" data-part=\"map-link\" href=\"https://www.google.com/maps/search/?api=1&query=41.0678,29.0092\" target=\"_blank\" rel=\"noopener noreferrer\">View on map</a></li><li data-store-locator-item=\"\" data-part=\"card\" style=\"display: flex; flex-direction: column; gap: 4px; padding-bottom: 12px;\"><span data-store-locator-name=\"\" data-part=\"name\" style=\"font-weight: 600;\">Ankamall</span><a data-store-locator-availability=\"\" data-store-locator-availability-state=\"unknown\" data-part=\"availability\" data-state=\"unknown\" href=\"https://brand.example/stores\" style=\"font-size: 0.85em;\">Check availability</a><span data-store-locator-address=\"\" data-part=\"address\" style=\"opacity: 0.8;\">Akköprü Mah.</span><span data-store-locator-city=\"\" data-part=\"city\" style=\"opacity: 0.7;\">Ankara</span><a data-store-locator-phone=\"\" data-part=\"phone\" href=\"tel:+903125411000\">+903125411000</a><a data-store-locator-map=\"\" data-part=\"map-link\" href=\"https://www.google.com/maps/search/?api=1&query=39.9497,32.8241\" target=\"_blank\" rel=\"noopener noreferrer\">View on map</a></li><li data-store-locator-item=\"\" data-part=\"card\" style=\"display: flex; flex-direction: column; gap: 4px; padding-bottom: 12px;\"><span data-store-locator-name=\"\" data-part=\"name\" style=\"font-weight: 600;\">Merkez</span><a data-store-locator-availability=\"\" data-store-locator-availability-state=\"unknown\" data-part=\"availability\" data-state=\"unknown\" href=\"https://brand.example/stores\" style=\"font-size: 0.85em;\">Check availability</a><span data-store-locator-city=\"\" data-part=\"city\" style=\"opacity: 0.7;\">İzmir</span></li></ul></div>"
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe('store locator — find in store is a dialog', () => {
|
|
233
|
+
it('renders the trigger byte for byte', () => {
|
|
234
|
+
expect(
|
|
235
|
+
canonical(renderBlock({ displayMode: 'trigger' }).container.innerHTML)
|
|
236
|
+
).toBe(canonical(GOLDEN['trigger']));
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('renders the open overlay byte for byte', () => {
|
|
240
|
+
const { container } = renderBlock({ displayMode: 'trigger' });
|
|
241
|
+
act(() => trigger(container).click());
|
|
242
|
+
|
|
243
|
+
expect(canonical(container.innerHTML)).toBe(
|
|
244
|
+
canonical(GOLDEN['trigger-open'])
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('renders the open overlay in the designer byte for byte', () => {
|
|
249
|
+
// Held open, placed as a side drawer, and — the line the golden exists to
|
|
250
|
+
// hold — with no `aria-modal`, because nothing traps focus on the canvas.
|
|
251
|
+
const { container } = renderBlock(
|
|
252
|
+
{ displayMode: 'trigger', panelPlacement: 'right' },
|
|
253
|
+
{ isDesigner: true }
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
expect(canonical(container.innerHTML)).toBe(
|
|
257
|
+
canonical(GOLDEN['trigger-open-designer'])
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('shows a trigger and nothing else until it is pressed', () => {
|
|
262
|
+
const { container } = renderBlock({ displayMode: 'trigger' });
|
|
263
|
+
|
|
264
|
+
expect(trigger(container)).toBeTruthy();
|
|
265
|
+
expect(trigger(container).getAttribute('aria-haspopup')).toBe('dialog');
|
|
266
|
+
expect(trigger(container).getAttribute('aria-expanded')).toBe('false');
|
|
267
|
+
expect(panel(container)).toBeNull();
|
|
268
|
+
expect(document.body.style.overflow).toBe('');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('moves focus in, locks the page and claims aria-modal honestly', () => {
|
|
272
|
+
const { container } = renderBlock({ displayMode: 'trigger' });
|
|
273
|
+
|
|
274
|
+
act(() => {
|
|
275
|
+
trigger(container).click();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const dialog = panel(container) as HTMLElement;
|
|
279
|
+
expect(dialog.getAttribute('role')).toBe('dialog');
|
|
280
|
+
expect(dialog.getAttribute('aria-modal')).toBe('true');
|
|
281
|
+
expect(dialog.getAttribute('aria-label')).toBe('Find in store');
|
|
282
|
+
expect(trigger(container).getAttribute('aria-expanded')).toBe('true');
|
|
283
|
+
expect(dialog.contains(document.activeElement)).toBe(true);
|
|
284
|
+
expect(document.body.style.overflow).toBe('hidden');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('holds Tab inside the dialog', () => {
|
|
288
|
+
const { container } = renderBlock({ displayMode: 'trigger' });
|
|
289
|
+
act(() => {
|
|
290
|
+
trigger(container).click();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const dialog = panel(container) as HTMLElement;
|
|
294
|
+
const focusables = Array.from(
|
|
295
|
+
dialog.querySelectorAll<HTMLElement>('a[href], button, select')
|
|
296
|
+
);
|
|
297
|
+
expect(focusables.length).toBeGreaterThan(1);
|
|
298
|
+
|
|
299
|
+
const first = focusables[0];
|
|
300
|
+
const last = focusables[focusables.length - 1];
|
|
301
|
+
|
|
302
|
+
act(() => last.focus());
|
|
303
|
+
fireEvent.keyDown(dialog, { key: 'Tab' });
|
|
304
|
+
expect(document.activeElement).toBe(first);
|
|
305
|
+
|
|
306
|
+
fireEvent.keyDown(dialog, { key: 'Tab', shiftKey: true });
|
|
307
|
+
expect(document.activeElement).toBe(last);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('pulls focus back when it lands outside the dialog', () => {
|
|
311
|
+
const outside = document.createElement('button');
|
|
312
|
+
document.body.appendChild(outside);
|
|
313
|
+
|
|
314
|
+
const { container } = renderBlock({ displayMode: 'trigger' });
|
|
315
|
+
act(() => {
|
|
316
|
+
trigger(container).click();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const dialog = panel(container) as HTMLElement;
|
|
320
|
+
act(() => outside.focus());
|
|
321
|
+
|
|
322
|
+
expect(dialog.contains(document.activeElement)).toBe(true);
|
|
323
|
+
outside.remove();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('closes on Escape and gives focus back to the trigger', () => {
|
|
327
|
+
const { container } = renderBlock({ displayMode: 'trigger' });
|
|
328
|
+
act(() => {
|
|
329
|
+
trigger(container).click();
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
act(() => {
|
|
333
|
+
fireEvent.keyDown(document, { key: 'Escape' });
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
expect(panel(container)).toBeNull();
|
|
337
|
+
expect(document.activeElement).toBe(trigger(container));
|
|
338
|
+
expect(document.body.style.overflow).toBe('');
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('closes on the close button and on the backdrop', () => {
|
|
342
|
+
const { container } = renderBlock({ displayMode: 'trigger' });
|
|
343
|
+
|
|
344
|
+
act(() => trigger(container).click());
|
|
345
|
+
act(() => {
|
|
346
|
+
(
|
|
347
|
+
container.querySelector(
|
|
348
|
+
'[data-store-locator-panel-close]'
|
|
349
|
+
) as HTMLElement
|
|
350
|
+
).click();
|
|
351
|
+
});
|
|
352
|
+
expect(panel(container)).toBeNull();
|
|
353
|
+
|
|
354
|
+
act(() => trigger(container).click());
|
|
355
|
+
act(() => {
|
|
356
|
+
(
|
|
357
|
+
container.querySelector('[data-store-locator-backdrop]') as HTMLElement
|
|
358
|
+
).click();
|
|
359
|
+
});
|
|
360
|
+
expect(panel(container)).toBeNull();
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('counts the holders of the scroll lock', () => {
|
|
364
|
+
// Two panels on one page is the ordinary composition, not a corner case:
|
|
365
|
+
// the mobile buy row carries one and the desktop one carries another.
|
|
366
|
+
const a = renderBlock({ displayMode: 'trigger' });
|
|
367
|
+
const b = renderBlock({ displayMode: 'trigger' });
|
|
368
|
+
|
|
369
|
+
act(() => trigger(a.container).click());
|
|
370
|
+
act(() => trigger(b.container).click());
|
|
371
|
+
expect(document.body.style.overflow).toBe('hidden');
|
|
372
|
+
|
|
373
|
+
b.unmount();
|
|
374
|
+
// The first panel is still over the page, so the lock is not the second
|
|
375
|
+
// panel's to release.
|
|
376
|
+
expect(document.body.style.overflow).toBe('hidden');
|
|
377
|
+
|
|
378
|
+
a.unmount();
|
|
379
|
+
expect(document.body.style.overflow).toBe('');
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('survives a second panel on the same page', () => {
|
|
383
|
+
// A product page carries this block twice — once on the mobile buy row and
|
|
384
|
+
// once on the desktop one — so two open panels is the ordinary
|
|
385
|
+
// composition. Each used to install a document-level `focusin` listener
|
|
386
|
+
// that pulled focus into its OWN panel, so the two pulled against each
|
|
387
|
+
// other until the tab died with "Maximum call stack size exceeded".
|
|
388
|
+
const outside = document.createElement('button');
|
|
389
|
+
document.body.appendChild(outside);
|
|
390
|
+
|
|
391
|
+
const a = renderBlock({ displayMode: 'trigger' });
|
|
392
|
+
const b = renderBlock({ displayMode: 'trigger' });
|
|
393
|
+
|
|
394
|
+
act(() => trigger(a.container).click());
|
|
395
|
+
act(() => trigger(b.container).click());
|
|
396
|
+
|
|
397
|
+
expect(() => act(() => outside.focus())).not.toThrow();
|
|
398
|
+
|
|
399
|
+
const second = panel(b.container) as HTMLElement;
|
|
400
|
+
// The panel a shopper opened last is the one holding focus.
|
|
401
|
+
expect(second.contains(document.activeElement)).toBe(true);
|
|
402
|
+
// And only that one may tell assistive tech the rest of the page is gone.
|
|
403
|
+
expect(second.getAttribute('aria-modal')).toBe('true');
|
|
404
|
+
expect(
|
|
405
|
+
(panel(a.container) as HTMLElement).getAttribute('aria-modal')
|
|
406
|
+
).toBeNull();
|
|
407
|
+
|
|
408
|
+
outside.remove();
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('closes the panel on top and leaves the one underneath open', () => {
|
|
412
|
+
const a = renderBlock({ displayMode: 'trigger' });
|
|
413
|
+
const b = renderBlock({ displayMode: 'trigger' });
|
|
414
|
+
|
|
415
|
+
act(() => trigger(a.container).click());
|
|
416
|
+
act(() => trigger(b.container).click());
|
|
417
|
+
|
|
418
|
+
act(() => {
|
|
419
|
+
fireEvent.keyDown(document, { key: 'Escape' });
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
expect(panel(b.container)).toBeNull();
|
|
423
|
+
expect(panel(a.container)).toBeTruthy();
|
|
424
|
+
// Escape backed out of one panel, not out of every panel on the page — so
|
|
425
|
+
// the page stays frozen behind the one still covering it, and that one
|
|
426
|
+
// takes the claim back now that it is on top.
|
|
427
|
+
expect(document.body.style.overflow).toBe('hidden');
|
|
428
|
+
expect(
|
|
429
|
+
(panel(a.container) as HTMLElement).getAttribute('aria-modal')
|
|
430
|
+
).toBe('true');
|
|
431
|
+
|
|
432
|
+
act(() => {
|
|
433
|
+
fireEvent.keyDown(document, { key: 'Escape' });
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
expect(panel(a.container)).toBeNull();
|
|
437
|
+
expect(document.body.style.overflow).toBe('');
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('restores the page value the first locker found, not an empty one', () => {
|
|
441
|
+
document.body.style.overflow = 'clip';
|
|
442
|
+
|
|
443
|
+
const { container } = renderBlock({ displayMode: 'trigger' });
|
|
444
|
+
act(() => trigger(container).click());
|
|
445
|
+
expect(document.body.style.overflow).toBe('hidden');
|
|
446
|
+
|
|
447
|
+
act(() => {
|
|
448
|
+
fireEvent.keyDown(document, { key: 'Escape' });
|
|
449
|
+
});
|
|
450
|
+
expect(document.body.style.overflow).toBe('clip');
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
describe('store locator — the designer is not a shopper', () => {
|
|
455
|
+
it('holds the panel open without claiming it, locking or stealing focus', () => {
|
|
456
|
+
const outside = document.createElement('button');
|
|
457
|
+
document.body.appendChild(outside);
|
|
458
|
+
act(() => outside.focus());
|
|
459
|
+
|
|
460
|
+
const { container } = renderBlock(
|
|
461
|
+
{ displayMode: 'trigger' },
|
|
462
|
+
{ isDesigner: true }
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
const dialog = panel(container) as HTMLElement;
|
|
466
|
+
expect(dialog).toBeTruthy();
|
|
467
|
+
expect(dialog.getAttribute('aria-modal')).toBeNull();
|
|
468
|
+
expect(document.activeElement).toBe(outside);
|
|
469
|
+
expect(document.body.style.overflow).toBe('');
|
|
470
|
+
|
|
471
|
+
// Nothing a click reaches may close the panel being styled.
|
|
472
|
+
act(() => trigger(container).click());
|
|
473
|
+
act(() =>
|
|
474
|
+
(
|
|
475
|
+
container.querySelector(
|
|
476
|
+
'[data-store-locator-panel-close]'
|
|
477
|
+
) as HTMLElement
|
|
478
|
+
).click()
|
|
479
|
+
);
|
|
480
|
+
act(() => {
|
|
481
|
+
fireEvent.keyDown(document, { key: 'Escape' });
|
|
482
|
+
});
|
|
483
|
+
expect(panel(container)).toBeTruthy();
|
|
484
|
+
|
|
485
|
+
outside.remove();
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it('asks commerce nothing and lets no link navigate', () => {
|
|
489
|
+
const { container } = renderBlock(
|
|
490
|
+
{
|
|
491
|
+
displayMode: 'trigger',
|
|
492
|
+
productMode: true,
|
|
493
|
+
availabilityHelpUrl: 'https://brand.example/stores'
|
|
494
|
+
},
|
|
495
|
+
{ isDesigner: true },
|
|
496
|
+
PRODUCT_CONTEXT
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
expect(stockCalls).toHaveLength(0);
|
|
500
|
+
container
|
|
501
|
+
.querySelectorAll('a[href]')
|
|
502
|
+
.forEach((anchor) => expect(anchor.getAttribute('href')).toBe('#'));
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
describe('store locator — an unasked store is unknown, not unavailable', () => {
|
|
507
|
+
const AVAILABILITY_PROPS = {
|
|
508
|
+
productMode: true,
|
|
509
|
+
availabilityHelpUrl: 'https://brand.example/stores'
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
const markers = (c: HTMLElement) =>
|
|
513
|
+
Object.fromEntries(
|
|
514
|
+
Array.from(
|
|
515
|
+
c.querySelectorAll('[data-store-locator-item]')
|
|
516
|
+
).map((card) => [
|
|
517
|
+
card.querySelector('[data-part="name"]')?.textContent,
|
|
518
|
+
card
|
|
519
|
+
.querySelector('[data-store-locator-availability]')
|
|
520
|
+
?.getAttribute('data-store-locator-availability-state')
|
|
521
|
+
])
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
beforeEach(() => {
|
|
525
|
+
mockStores = [
|
|
526
|
+
...GOLDEN_STORES,
|
|
527
|
+
{
|
|
528
|
+
pk: 4,
|
|
529
|
+
name: 'Akasya',
|
|
530
|
+
address: 'Acıbadem',
|
|
531
|
+
latitude: '40.9910',
|
|
532
|
+
longitude: '29.0630',
|
|
533
|
+
township: { name: 'Üsküdar', city: { name: 'İstanbul' } }
|
|
534
|
+
}
|
|
535
|
+
];
|
|
536
|
+
mockCities = [{ pk: 34, name: 'İstanbul' }];
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
const chooseIstanbul = (container: HTMLElement) =>
|
|
540
|
+
fireEvent.change(container.querySelector('select') as HTMLSelectElement, {
|
|
541
|
+
target: { value: 'İstanbul' }
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it('marks only what the city answer actually mentioned', async () => {
|
|
545
|
+
mockStockRows = [{ name: 'Kanyon AVM', stock: '3' }];
|
|
546
|
+
|
|
547
|
+
const { container } = renderBlock(AVAILABILITY_PROPS, {}, PRODUCT_CONTEXT);
|
|
548
|
+
chooseIstanbul(container);
|
|
549
|
+
|
|
550
|
+
await waitFor(() =>
|
|
551
|
+
expect(markers(container)['Kanyon AVM']).toBe('available')
|
|
552
|
+
);
|
|
553
|
+
// Akasya is in the answered city and the answer never named it. That is
|
|
554
|
+
// not a no — it is a question nobody asked.
|
|
555
|
+
expect(markers(container)['Akasya']).toBe('unknown');
|
|
556
|
+
expect(
|
|
557
|
+
container.querySelector(
|
|
558
|
+
'[data-store-locator-availability-state="unavailable"]'
|
|
559
|
+
)
|
|
560
|
+
).toBeNull();
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it('says unavailable only when commerce said zero', async () => {
|
|
564
|
+
mockStockRows = [
|
|
565
|
+
{ name: 'Kanyon AVM', stock: '0' },
|
|
566
|
+
{ name: 'Akasya', stock: 'n/a' }
|
|
567
|
+
];
|
|
568
|
+
|
|
569
|
+
const { container } = renderBlock(AVAILABILITY_PROPS, {}, PRODUCT_CONTEXT);
|
|
570
|
+
chooseIstanbul(container);
|
|
571
|
+
|
|
572
|
+
await waitFor(() =>
|
|
573
|
+
expect(markers(container)['Kanyon AVM']).toBe('unavailable')
|
|
574
|
+
);
|
|
575
|
+
// A count commerce could not express is not a zero either.
|
|
576
|
+
expect(markers(container)['Akasya']).toBe('unknown');
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
it('keeps every store unknown when the request fails', async () => {
|
|
580
|
+
mockStockRejects = true;
|
|
581
|
+
|
|
582
|
+
const { container } = renderBlock(AVAILABILITY_PROPS, {}, PRODUCT_CONTEXT);
|
|
583
|
+
chooseIstanbul(container);
|
|
584
|
+
|
|
585
|
+
await waitFor(() => expect(markers(container)['Kanyon AVM']).toBe('unknown'));
|
|
586
|
+
expect(markers(container)['Akasya']).toBe('unknown');
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it('gives the unknown marker its own state to be styled by', async () => {
|
|
590
|
+
mockStockRows = [{ name: 'Kanyon AVM', stock: '3' }];
|
|
591
|
+
|
|
592
|
+
const { container } = renderBlock(AVAILABILITY_PROPS, {}, PRODUCT_CONTEXT);
|
|
593
|
+
chooseIstanbul(container);
|
|
594
|
+
|
|
595
|
+
await waitFor(() =>
|
|
596
|
+
expect(markers(container)['Kanyon AVM']).toBe('available')
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
const unknown = Array.from(
|
|
600
|
+
container.querySelectorAll('[data-part="availability"]')
|
|
601
|
+
).filter((el) => el.getAttribute('data-state') === 'unknown');
|
|
602
|
+
const unavailable = Array.from(
|
|
603
|
+
container.querySelectorAll('[data-part="availability"]')
|
|
604
|
+
).filter((el) => el.getAttribute('data-state') === 'unavailable');
|
|
605
|
+
|
|
606
|
+
expect(unknown.length).toBeGreaterThan(0);
|
|
607
|
+
expect(unavailable).toHaveLength(0);
|
|
608
|
+
// The unknown state carries the brand's route rather than a dead label.
|
|
609
|
+
expect(unknown[0].tagName).toBe('A');
|
|
610
|
+
expect(unknown[0].getAttribute('href')).toBe('https://brand.example/stores');
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
describe('store locator — the check-availability route reaches the overlay', () => {
|
|
615
|
+
it('offers the route beside the notice, inside the panel', () => {
|
|
616
|
+
const { container } = renderBlock(
|
|
617
|
+
{
|
|
618
|
+
displayMode: 'trigger',
|
|
619
|
+
productMode: true,
|
|
620
|
+
availabilityHelpUrl: 'https://brand.example/stores'
|
|
621
|
+
},
|
|
622
|
+
{},
|
|
623
|
+
PRODUCT_CONTEXT
|
|
624
|
+
);
|
|
625
|
+
|
|
626
|
+
act(() => trigger(container).click());
|
|
627
|
+
|
|
628
|
+
const dialog = panel(container) as HTMLElement;
|
|
629
|
+
const route = dialog.querySelector(
|
|
630
|
+
'[data-store-locator-notice-link]'
|
|
631
|
+
) as HTMLAnchorElement;
|
|
632
|
+
|
|
633
|
+
expect(dialog.querySelector('[data-store-locator-notice]')).toBeTruthy();
|
|
634
|
+
expect(route).toBeTruthy();
|
|
635
|
+
expect(route.getAttribute('href')).toBe('https://brand.example/stores');
|
|
636
|
+
// Same wording as the unknown marker — one authorable label, not two.
|
|
637
|
+
expect(route.textContent).toBe('Check availability');
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it('offers it when the overlay has no store to show at all', () => {
|
|
641
|
+
mockStores = [];
|
|
642
|
+
|
|
643
|
+
const { container } = renderBlock(
|
|
644
|
+
{
|
|
645
|
+
displayMode: 'trigger',
|
|
646
|
+
productMode: true,
|
|
647
|
+
availabilityHelpUrl: 'https://brand.example/stores'
|
|
648
|
+
},
|
|
649
|
+
{},
|
|
650
|
+
PRODUCT_CONTEXT
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
act(() => trigger(container).click());
|
|
654
|
+
|
|
655
|
+
const dialog = panel(container) as HTMLElement;
|
|
656
|
+
expect(dialog.querySelector('[data-part="empty"]')).toBeTruthy();
|
|
657
|
+
expect(
|
|
658
|
+
dialog
|
|
659
|
+
.querySelector('[data-store-locator-notice-link]')
|
|
660
|
+
?.getAttribute('href')
|
|
661
|
+
).toBe('https://brand.example/stores');
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it('stays out of the empty message a plain store list renders', () => {
|
|
665
|
+
mockStores = [];
|
|
666
|
+
|
|
667
|
+
const { container } = renderBlock({ showEmptyState: true });
|
|
668
|
+
|
|
669
|
+
expect(container.querySelector('[data-part="empty"]')).toBeTruthy();
|
|
670
|
+
expect(container.querySelector('[data-store-locator-notice-link]')).toBeNull();
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
it('refuses a url that would execute', () => {
|
|
674
|
+
// The url is typed by a merchant in the editor and lands in an href.
|
|
675
|
+
const { container } = renderBlock(
|
|
676
|
+
{
|
|
677
|
+
productMode: true,
|
|
678
|
+
availabilityHelpUrl: 'java\u0000script:alert(1)'
|
|
679
|
+
},
|
|
680
|
+
{},
|
|
681
|
+
PRODUCT_CONTEXT
|
|
682
|
+
);
|
|
683
|
+
|
|
684
|
+
expect(container.querySelector('[data-store-locator-notice-link]')).toBeNull();
|
|
685
|
+
expect(container.querySelector('a[href^="java"]')).toBeNull();
|
|
686
|
+
// The label survives as text, which is what an unset url does too.
|
|
687
|
+
const marker = container.querySelector('[data-part="availability"]');
|
|
688
|
+
expect(marker?.tagName).toBe('SPAN');
|
|
689
|
+
expect(marker?.textContent).toBe('Check availability');
|
|
690
|
+
});
|
|
691
|
+
});
|