@@ -221,16 +221,16 @@ public function testJsonResponseWithJsVarnameUsesCompressionToPreventSessionBloa
221221 $ backendSession ->hasUpdateResult (),
222222 'Session should contain compressed updateResult '
223223 );
224-
224+
225225 $ sessionData = $ backendSession ->getUpdateResult ();
226226 $ this ->assertIsArray ($ sessionData , 'Session data should be array for compressed format ' );
227227 $ this ->assertTrue ($ sessionData ['compressed ' ], 'Data should be marked as compressed ' );
228228 $ this ->assertArrayHasKey ('data ' , $ sessionData , 'Compressed data should exist ' );
229-
229+
230230 // Verify compression actually reduces size
231231 $ decompressed = gzdecode ($ sessionData ['data ' ]);
232232 $ this ->assertNotEmpty ($ decompressed , 'Decompressed data should not be empty ' );
233-
233+
234234 $ originalSize = strlen ($ decompressed );
235235 $ compressedSize = strlen ($ sessionData ['data ' ]);
236236 $ this ->assertLessThan (
@@ -346,6 +346,107 @@ public function testAddProductToOrderFromWishList(): void
346346 $ this ->assertCount (1 , $ quoteItems );
347347 }
348348
349+ /**
350+ * Verify that when another product is added from the grid, a previously configured product
351+ * (with required options) is NOT re-added due to presence of options[files_prefix] only.
352+ *
353+ * Uses new DataFixture-based products to avoid URL rewrite collisions.
354+ */
355+ public function testGridAddSkipsConfiguredProductWithoutOptions (): void
356+ {
357+ /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */
358+ $ productRepository = $ this ->_objectManager ->get (\Magento \Catalog \Api \ProductRepositoryInterface::class);
359+ // Create two fresh products (unique skus) using Product fixture
360+ /** @var \Magento\Catalog\Test\Fixture\Product $productFixture */
361+ $ productFixture = $ this ->_objectManager ->get (\Magento \Catalog \Test \Fixture \Product::class);
362+ $ productFixture ->apply (['sku ' => 'custom_options_p1 ' , 'price ' => 10 ]);
363+ $ productFixture ->apply (['sku ' => 'plain_p1 ' , 'price ' => 10 ]);
364+ $ customProduct = $ productRepository ->get ('custom_options_p1 ' );
365+ $ simpleProduct = $ productRepository ->get ('plain_p1 ' );
366+
367+ // Programmatically add one required text custom option to the custom product
368+ /** @var \Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory $customOptionFactory */
369+ $ customOptionFactory = $ this ->_objectManager
370+ ->get (\Magento \Catalog \Api \Data \ProductCustomOptionInterfaceFactory::class);
371+ $ requiredTextOption = $ customOptionFactory ->create (
372+ [
373+ 'data ' => [
374+ 'title ' => 'Req Text ' ,
375+ 'type ' => 'field ' ,
376+ 'is_require ' => 1 ,
377+ 'sort_order ' => 0 ,
378+ 'price ' => 1 ,
379+ 'price_type ' => 'fixed ' ,
380+ 'sku ' => 'opt_text_1 ' ,
381+ 'max_characters ' => 100 ,
382+ ],
383+ ]
384+ );
385+ $ requiredTextOption ->setProductSku ($ customProduct ->getSku ());
386+ $ customProduct ->setCanSaveCustomOptions (true )->setHasOptions (true );
387+ $ customProduct ->setOptions ([$ requiredTextOption ]);
388+ $ productRepository ->save ($ customProduct );
389+
390+ // Add the product with required custom options to the admin create quote with qty 10
391+ $ options = [];
392+ foreach ($ customProduct ->getOptions () as $ option ) {
393+ $ value = null ;
394+ if ($ option ->getValues ()) {
395+ $ values = $ option ->getValues ();
396+ $ first = reset ($ values );
397+ $ value = $ first ? (int )$ first ->getOptionTypeId () : null ;
398+ }
399+ if ($ value === null ) {
400+ $ value = 'test ' ;
401+ }
402+ $ options [(int )$ option ->getId ()] = $ value ;
403+ }
404+ /** @var \Magento\Sales\Model\AdminOrder\Create $orderCreate */
405+ $ orderCreate = $ this ->_objectManager ->get (\Magento \Sales \Model \AdminOrder \Create::class);
406+ $ orderCreate ->addProduct (
407+ (int )$ customProduct ->getId (),
408+ ['qty ' => 10 , 'options ' => $ options ]
409+ );
410+
411+ // Emulate grid submit where the first (already configured) product comes with only files_prefix.
412+ // Avoid rendering the sidebar (to prevent wishlist creation) and do not set a customer in session.
413+ $ params = $ this ->hydrateParams ([
414+ 'block ' => 'items,shipping_method,billing_method,totals,giftmessage '
415+ ]);
416+ $ post = $ this ->hydratePost ([
417+ 'customer_id ' => 0 ,
418+ 'item ' => [
419+ (int )$ customProduct ->getId () => [
420+ 'options ' => ['files_prefix ' => 'item_ ' . (int )$ customProduct ->getId () . '_ ' ],
421+ ],
422+ (int )$ simpleProduct ->getId () => ['qty ' => 1 ],
423+ ],
424+ ]);
425+ $ this ->dispatchWitParams ($ params , $ post );
426+
427+ // Assert custom product wasn't re-added and quantities are as expected
428+ $ quote = $ this ->session ->getQuote ();
429+ $ items = $ quote ->getItemsCollection (false )->getItems ();
430+ $ customCount = 0 ;
431+ $ customQty = 0.0 ;
432+ $ simpleCount = 0 ;
433+ $ simpleQty = 0.0 ;
434+ foreach ($ items as $ item ) {
435+ if ((int )$ item ->getProductId () === (int )$ customProduct ->getId ()) {
436+ $ customCount ++;
437+ $ customQty += (float )$ item ->getQty ();
438+ }
439+ if ((int )$ item ->getProductId () === (int )$ simpleProduct ->getId ()) {
440+ $ simpleCount ++;
441+ $ simpleQty += (float )$ item ->getQty ();
442+ }
443+ }
444+ $ this ->assertEquals (1 , $ customCount );
445+ $ this ->assertEquals (10.0 , $ customQty );
446+ $ this ->assertEquals (1 , $ simpleCount );
447+ $ this ->assertEquals (1.0 , $ simpleQty );
448+ }
449+
349450 /**
350451 * Check that customer notification is NOT disabled after comment is updated.
351452 *
0 commit comments