Skip to content

Commit 5c6d83d

Browse files
authored
LYNX-603: Product attribute > trademark short form ™ is returned as ™[Fix]
1 parent 718c64f commit 5c6d83d

File tree

3 files changed

+201
-3
lines changed

3 files changed

+201
-3
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\CatalogGraphQl\Model\Resolver\Product;
9+
10+
use Magento\Framework\Escaper;
11+
use Magento\Framework\Exception\LocalizedException;
12+
use Magento\Framework\GraphQl\Config\Element\Field;
13+
use Magento\Framework\GraphQl\Query\ResolverInterface;
14+
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
15+
16+
class ProductName implements ResolverInterface
17+
{
18+
/**
19+
* ProductName constructor
20+
*
21+
* @param Escaper $escaper
22+
*/
23+
public function __construct(
24+
private readonly Escaper $escaper
25+
) {
26+
}
27+
28+
/**
29+
* @inheritdoc
30+
*
31+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
32+
*/
33+
public function resolve(
34+
Field $field,
35+
$context,
36+
ResolveInfo $info,
37+
array $value = null,
38+
array $args = null
39+
): string {
40+
if (!isset($value['model'])) {
41+
throw new LocalizedException(__('"model" value should be specified'));
42+
}
43+
44+
// Product name allowed with special characters
45+
return str_replace(
46+
"&apos;",
47+
"'",
48+
str_replace("&amp;", "&", $this->escaper->escapeUrl($value['model']->getName()))
49+
);
50+
}
51+
}

app/code/Magento/CatalogGraphQl/etc/schema.graphqls

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
# Copyright © Magento, Inc. All rights reserved.
2-
# See COPYING.txt for license details.
1+
# Copyright 2025 Adobe
2+
# All Rights Reserved.
33

44
type Query {
55
products (
@@ -93,7 +93,7 @@ interface ProductLinksInterface @typeResolver(class: "Magento\\CatalogGraphQl\\M
9393
interface ProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\ProductInterfaceTypeResolverComposite") @doc(description: "Contains fields that are common to all types of products.") {
9494
id: Int @deprecated(reason: "Use the `uid` field instead.") @doc(description: "The ID number assigned to the product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\EntityIdToId")
9595
uid: ID! @doc(description: "The unique ID for a `ProductInterface` object.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\EntityIdToUid")
96-
name: String @doc(description: "The product name. Customers use this name to identify the product.")
96+
name: String @doc(description: "The product name. Customers use this name to identify the product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductName")
9797
sku: String @doc(description: "A number or code assigned to a product to identify the product, options, price, and manufacturer.")
9898
description: ComplexTextValue @doc(description: "Detailed information about the product. The value can include simple HTML tags.") @resolver(class: "\\Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductComplexTextAttribute")
9999
short_description: ComplexTextValue @doc(description: "A short description of the product. Its use depends on the theme.") @resolver(class: "\\Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductComplexTextAttribute")
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\GraphQl\Catalog;
9+
10+
use Magento\Catalog\Test\Fixture\Product as ProductFixture;
11+
use Magento\Framework\Exception\NoSuchEntityException;
12+
use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface;
13+
use Magento\Quote\Test\Fixture\GuestCart as GuestCartFixture;
14+
use Magento\TestFramework\Fixture\DataFixture;
15+
use Magento\TestFramework\Fixture\DataFixtureStorage;
16+
use Magento\TestFramework\Fixture\DataFixtureStorageManager;
17+
use Magento\TestFramework\Helper\Bootstrap;
18+
use Magento\TestFramework\TestCase\GraphQlAbstract;
19+
20+
class ProductNameWithSpecialCharactersTest extends GraphQlAbstract
21+
{
22+
/**
23+
* @var QuoteIdToMaskedQuoteIdInterface
24+
*/
25+
private $quoteIdToMaskedQuoteId;
26+
27+
/**
28+
* @var DataFixtureStorage
29+
*/
30+
private $fixtures;
31+
32+
/**
33+
* @inheritdoc
34+
*/
35+
protected function setUp(): void
36+
{
37+
$this->quoteIdToMaskedQuoteId = Bootstrap::getObjectManager()->get(QuoteIdToMaskedQuoteIdInterface::class);
38+
$this->fixtures = Bootstrap::getObjectManager()->get(DataFixtureStorageManager::class)->getStorage();
39+
}
40+
41+
/**
42+
* Test product name with special characters
43+
*
44+
* @param string $sku
45+
* @param string $expectedName
46+
* @throws NoSuchEntityException
47+
* @dataProvider productNameProvider
48+
*/
49+
#[
50+
DataFixture(ProductFixture::class, [
51+
'sku' => 'test-product-1',
52+
'name' => 'Test Product© 1'
53+
]),
54+
DataFixture(ProductFixture::class, [
55+
'sku' => 'test-product-2',
56+
'name' => 'Test Product™ 2'
57+
]),
58+
DataFixture(ProductFixture::class, [
59+
'sku' => 'test-product-3',
60+
'name' => 'Sample Product&copy; 3'
61+
]),
62+
DataFixture(ProductFixture::class, [
63+
'sku' => 'test-product-4',
64+
'name' => 'Sample Product&trade; 4'
65+
]),
66+
DataFixture(ProductFixture::class, [
67+
'sku' => 'test-product-5',
68+
'name' => 'Test Product 5'
69+
]),
70+
DataFixture(GuestCartFixture::class, as: 'cart')
71+
]
72+
public function testProductName(string $sku, string $expectedName): void
73+
{
74+
$maskedQuoteId = $this->quoteIdToMaskedQuoteId->execute(
75+
(int)$this->fixtures->get('cart')->getId()
76+
);
77+
78+
$response = $this->graphQlMutation($this->getAddToCartMutation($maskedQuoteId, $sku));
79+
80+
self::assertEquals(
81+
[
82+
'cart' => [
83+
'items' => [
84+
[
85+
'quantity' => 1,
86+
'product' => [
87+
'sku' => $sku,
88+
'name' => $expectedName
89+
]
90+
]
91+
]
92+
]
93+
],
94+
$response['addProductsToCart']
95+
);
96+
}
97+
98+
/**
99+
* Data provider for product name test cases
100+
*
101+
* @return array[]
102+
*/
103+
public static function productNameProvider(): array
104+
{
105+
return [
106+
['test-product-1', 'Test Product© 1'],
107+
['test-product-2', 'Test Product™ 2'],
108+
['test-product-3', 'Sample Product© 3'],
109+
['test-product-4', 'Sample Product™ 4'],
110+
['test-product-5', 'Test Product 5']
111+
];
112+
}
113+
114+
/**
115+
* Returns Add to cart mutation
116+
*
117+
* @param string $maskedQuoteId
118+
* @param string $sku
119+
* @return string
120+
*/
121+
private function getAddToCartMutation(string $maskedQuoteId, string $sku): string
122+
{
123+
return <<<MUTATION
124+
mutation {
125+
addProductsToCart(
126+
cartId: "{$maskedQuoteId}",
127+
cartItems: [
128+
{
129+
sku: "{$sku}"
130+
quantity: 1
131+
}
132+
]
133+
) {
134+
cart {
135+
items {
136+
quantity
137+
product {
138+
sku
139+
name
140+
}
141+
}
142+
}
143+
}
144+
}
145+
MUTATION;
146+
}
147+
}

0 commit comments

Comments
 (0)