@ordergroove/offers 2.48.0 → 2.48.1-alpha-PR-1371-2.308

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ordergroove/offers",
3
- "version": "2.48.0",
3
+ "version": "2.48.1-alpha-PR-1371-2.308+bb24afe7a",
4
4
  "description": "offer state component",
5
5
  "author": "Eugenio Lattanzio <eugenio63@gmail.com>",
6
6
  "homepage": "https://github.com/ordergroove/plush-toys#readme",
@@ -49,5 +49,5 @@
49
49
  "@ordergroove/offers-templates": "^0.10.0",
50
50
  "@types/lodash.memoize": "^4.1.9"
51
51
  },
52
- "gitHead": "8ec942439e46e09a4970c14c0faa93bba6975f31"
52
+ "gitHead": "bb24afe7a715c5f0ea03ab04992bd4bc25cd648b"
53
53
  }
@@ -11,7 +11,8 @@ import {
11
11
  synchronizeCartOptin,
12
12
  getTrackingEvent,
13
13
  guessProductHandle,
14
- synchronizeSellingPlan
14
+ synchronizeSellingPlan,
15
+ setupPdp
15
16
  } from '../shopifyMiddleware';
16
17
  import { getOrCreateHidden } from '../../core/utils';
17
18
 
@@ -508,3 +509,112 @@ describe('synchronizeSellingPlan', () => {
508
509
  expect(getFormData(formElement)).toEqual({});
509
510
  });
510
511
  });
512
+
513
+ describe('setupPdp', () => {
514
+ let store = { dispatch: () => {} };
515
+ let offer;
516
+
517
+ function makeCartAddForm() {
518
+ const element = document.createElement('form');
519
+ element.action = '/cart/add';
520
+ element.innerHTML = `
521
+ <input value="variant-1" name="id" type="hidden">
522
+ <input value="2" name="quantity" type="hidden">
523
+ `;
524
+ document.body.appendChild(element);
525
+ return element;
526
+ }
527
+
528
+ // wait for the debounced syncProductId function to run
529
+ async function waitForDebounce() {
530
+ // order matters here - this allows the MutationObserver callback to run, which queues a syncProductId call
531
+ await Promise.resolve();
532
+ // then wait for the debounced function to execute
533
+ jasmine.clock().tick(100);
534
+ }
535
+
536
+ beforeEach(() => {
537
+ offer = document.createElement('og-offer');
538
+ offer.dataset.shopifyProductHandle = 'test-product-handle';
539
+ spyOn(offer, 'setAttribute').and.callThrough();
540
+
541
+ let form = makeCartAddForm();
542
+ form.append(offer);
543
+
544
+ fetchMock.route('/products/test-product-handle.js', {}, { method: 'GET' });
545
+ fetchMock.route('/cart.js', {}, { method: 'GET' });
546
+
547
+ fetchMock.mockGlobal();
548
+ jasmine.clock().install();
549
+ });
550
+
551
+ afterEach(() => {
552
+ fetchMock.removeRoutes().unmockGlobal();
553
+ jasmine.clock().uninstall();
554
+ });
555
+
556
+ it('should guess handle and fetch data', async () => {
557
+ await setupPdp(store, offer);
558
+
559
+ expect(
560
+ fetchMock.callHistory.called({
561
+ url: 'path:/products/test-product-handle.js'
562
+ })
563
+ ).toBe(true);
564
+ });
565
+
566
+ it('should update product attribute when hidden ID input is modified', async () => {
567
+ await setupPdp(store, offer);
568
+
569
+ const form = offer.closest('form');
570
+ // update variant ID
571
+ const idInput = form.querySelector('input[name="id"]');
572
+ idInput.value = 'variant-2';
573
+
574
+ await waitForDebounce();
575
+
576
+ expect(offer.setAttribute).toHaveBeenCalledWith('product', 'variant-2');
577
+ });
578
+
579
+ it('does not update product attribute if value attribute changes on a non-id input', async () => {
580
+ await setupPdp(store, offer);
581
+
582
+ const form = offer.closest('form');
583
+ const quantityInput = form.querySelector('input[name="quantity"]');
584
+ quantityInput.value = 5;
585
+
586
+ await waitForDebounce();
587
+
588
+ expect(offer.setAttribute).not.toHaveBeenCalled();
589
+ });
590
+
591
+ // this test is about covering existing behavior - in the future we may be able to strictly rely on the attribute change instead of subtree modifications
592
+ it('should update product attribute when node is added', async () => {
593
+ await setupPdp(store, offer);
594
+
595
+ const form = offer.closest('form');
596
+ const newInput = document.createElement('input');
597
+ newInput.name = 'other';
598
+ newInput.value = 'some-value';
599
+
600
+ form.appendChild(newInput);
601
+
602
+ await waitForDebounce();
603
+
604
+ // called with existing value in form
605
+ expect(offer.setAttribute).toHaveBeenCalledWith('product', 'variant-1');
606
+ });
607
+
608
+ // this test is about covering existing behavior - theoretically watching for changes to the "value" attribute should be sufficient
609
+ it('should update product attribute when change event fired', async () => {
610
+ await setupPdp(store, offer);
611
+ const form = offer.closest('form');
612
+
613
+ form.dispatchEvent(new Event('change'));
614
+
615
+ await waitForDebounce();
616
+
617
+ // called with existing value in form
618
+ expect(offer.setAttribute).toHaveBeenCalledWith('product', 'variant-1');
619
+ });
620
+ });
@@ -53,7 +53,7 @@ async function getCurrency() {
53
53
  return cart.currency;
54
54
  }
55
55
 
56
- async function setupPdp(store, offer) {
56
+ export async function setupPdp(store, offer) {
57
57
  const handle = guessProductHandle(offer);
58
58
  if (handle) {
59
59
  try {
@@ -85,11 +85,26 @@ async function setupPdp(store, offer) {
85
85
  }
86
86
 
87
87
  if (form) {
88
- // since syncProductId is debounced not matter which comes first mutation or onchange
88
+ // this code watches the cart form for changes, so that we can update the product ID on the offer when the variant changes
89
89
  const syncProductId = makeSyncProductId(offer);
90
+ // watch the cart form for changes
91
+ // note: changes to hidden inputs do not fire change events
90
92
  form.addEventListener('change', () => syncProductId(form));
91
- const mo = new MutationObserver(() => syncProductId(form));
92
- mo.observe(form, { subtree: true, childList: true });
93
+ // watch for added/removed nodes anywhere in the cart form (including updates to text content)
94
+ // also watch for changes to the "value" attribute, which will catch changes to hidden inputs
95
+ const mo = new MutationObserver(e => {
96
+ // if we're only looking at attribute changes
97
+ if (e.every(it => it.type === 'attributes')) {
98
+ // sync the product ID only if the "id" input changed - this happens when the product variant changes
99
+ // for performance reasons, avoid reacting to every form attribute change
100
+ if (e.some(it => (it.target as HTMLInputElement).name === 'id')) {
101
+ syncProductId(form);
102
+ }
103
+ } else {
104
+ syncProductId(form);
105
+ }
106
+ });
107
+ mo.observe(form, { subtree: true, childList: true, attributes: true, attributeFilter: ['value'] });
93
108
  } else {
94
109
  console.info('no /cart/add form found for og-offer', offer);
95
110
  }