order-line-custom-fields.e2e-spec.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. import { mergeConfig, Product } from '@vendure/core';
  2. import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
  3. import path from 'path';
  4. import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
  5. import { initialData } from '../../../e2e-common/e2e-initial-data';
  6. import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
  7. import { FragmentOf, graphql } from './graphql/graphql-shop';
  8. import { fixPostgresTimezone } from './utils/fix-pg-timezone';
  9. const orderWithCustomFieldsFragment = graphql(`
  10. fragment OrderWithCustomFields on Order {
  11. id
  12. lines {
  13. id
  14. quantity
  15. customFields {
  16. stringField
  17. intField
  18. booleanField
  19. nullableField
  20. relationField {
  21. id
  22. name
  23. }
  24. }
  25. }
  26. }
  27. `);
  28. const addItemToOrderWithCustomFieldsDocument = graphql(
  29. `
  30. mutation AddItemToOrderWithCustomFields(
  31. $productVariantId: ID!
  32. $quantity: Int!
  33. $customFields: OrderLineCustomFieldsInput
  34. ) {
  35. addItemToOrder(
  36. productVariantId: $productVariantId
  37. quantity: $quantity
  38. customFields: $customFields
  39. ) {
  40. ...OrderWithCustomFields
  41. ... on ErrorResult {
  42. errorCode
  43. message
  44. }
  45. }
  46. }
  47. `,
  48. [orderWithCustomFieldsFragment],
  49. );
  50. const adjustOrderLineWithCustomFieldsDocument = graphql(
  51. `
  52. mutation AdjustOrderLineWithCustomFields(
  53. $orderLineId: ID!
  54. $quantity: Int!
  55. $customFields: OrderLineCustomFieldsInput
  56. ) {
  57. adjustOrderLine(orderLineId: $orderLineId, quantity: $quantity, customFields: $customFields) {
  58. ...OrderWithCustomFields
  59. ... on ErrorResult {
  60. errorCode
  61. message
  62. }
  63. }
  64. }
  65. `,
  66. [orderWithCustomFieldsFragment],
  67. );
  68. const removeAllOrderLinesDocument = graphql(`
  69. mutation RemoveAllOrderLines {
  70. removeAllOrderLines {
  71. ... on Order {
  72. id
  73. lines {
  74. id
  75. quantity
  76. }
  77. }
  78. ... on ErrorResult {
  79. errorCode
  80. message
  81. }
  82. }
  83. }
  84. `);
  85. type OrderWithCustomFields = FragmentOf<typeof orderWithCustomFieldsFragment>;
  86. const orderGuard: ErrorResultGuard<OrderWithCustomFields> = createErrorResultGuard(input => !!input.lines);
  87. fixPostgresTimezone();
  88. const customConfig = mergeConfig(testConfig(), {
  89. customFields: {
  90. OrderLine: [
  91. { name: 'stringField', type: 'string' },
  92. { name: 'intField', type: 'int' },
  93. { name: 'booleanField', type: 'boolean' },
  94. { name: 'nullableField', type: 'string', nullable: true },
  95. { name: 'relationField', type: 'relation', entity: Product },
  96. ],
  97. },
  98. });
  99. describe('OrderLine Custom Fields', () => {
  100. const { server, adminClient, shopClient } = createTestEnvironment(customConfig);
  101. beforeAll(async () => {
  102. await server.init({
  103. initialData,
  104. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
  105. customerCount: 1,
  106. });
  107. await adminClient.asSuperAdmin();
  108. }, TEST_SETUP_TIMEOUT_MS);
  109. afterAll(async () => {
  110. await server.destroy();
  111. });
  112. beforeEach(async () => {
  113. // Clear the shopping cart before each test to ensure test isolation
  114. await shopClient.query(removeAllOrderLinesDocument);
  115. });
  116. describe('addItemToOrder', () => {
  117. it('can add order line with custom fields', async () => {
  118. const { addItemToOrder } = await shopClient.query(addItemToOrderWithCustomFieldsDocument, {
  119. productVariantId: 'T_1',
  120. quantity: 1,
  121. customFields: { stringField: 'test value', intField: 42, booleanField: true },
  122. });
  123. orderGuard.assertSuccess(addItemToOrder);
  124. expect(addItemToOrder.lines[0].customFields).toEqual({
  125. stringField: 'test value',
  126. intField: 42,
  127. booleanField: true,
  128. nullableField: null,
  129. relationField: null,
  130. });
  131. });
  132. it('can add order line with relation custom field', async () => {
  133. const { addItemToOrder } = await shopClient.query(addItemToOrderWithCustomFieldsDocument, {
  134. productVariantId: 'T_2',
  135. quantity: 1,
  136. customFields: { relationFieldId: 'T_1' },
  137. });
  138. orderGuard.assertSuccess(addItemToOrder);
  139. expect(addItemToOrder.lines[0].customFields.relationField.id).toBe('T_1');
  140. });
  141. });
  142. describe('adjustOrderLine - merging behavior', () => {
  143. it('should merge custom fields when updating partial fields', async () => {
  144. // Create a fresh order line for this test
  145. const { addItemToOrder } = await shopClient.query(addItemToOrderWithCustomFieldsDocument, {
  146. productVariantId: 'T_3',
  147. quantity: 1,
  148. customFields: {
  149. stringField: 'initial value',
  150. intField: 100,
  151. booleanField: false,
  152. nullableField: 'not null',
  153. },
  154. });
  155. orderGuard.assertSuccess(addItemToOrder);
  156. const orderLineId = addItemToOrder.lines[0].id;
  157. const { adjustOrderLine } = await shopClient.query(adjustOrderLineWithCustomFieldsDocument, {
  158. orderLineId,
  159. quantity: 2,
  160. customFields: {
  161. stringField: 'updated value',
  162. },
  163. });
  164. orderGuard.assertSuccess(adjustOrderLine);
  165. const updatedLine = adjustOrderLine.lines.find(line => line.id === orderLineId);
  166. expect(updatedLine.customFields).toEqual({
  167. stringField: 'updated value', // updated
  168. intField: 100, // preserved
  169. booleanField: false, // preserved
  170. nullableField: 'not null', // preserved
  171. relationField: null, // preserved
  172. });
  173. });
  174. it('should allow updating multiple fields while preserving others', async () => {
  175. // Create a fresh order line for this test
  176. const { addItemToOrder } = await shopClient.query(addItemToOrderWithCustomFieldsDocument, {
  177. productVariantId: 'T_4',
  178. quantity: 1,
  179. customFields: {
  180. stringField: 'initial value',
  181. intField: 100,
  182. booleanField: false,
  183. nullableField: 'not null',
  184. },
  185. });
  186. orderGuard.assertSuccess(addItemToOrder);
  187. const orderLineId = addItemToOrder.lines[0].id;
  188. const { adjustOrderLine } = await shopClient.query(adjustOrderLineWithCustomFieldsDocument, {
  189. orderLineId,
  190. quantity: 2,
  191. customFields: {
  192. intField: 200,
  193. booleanField: true,
  194. },
  195. });
  196. orderGuard.assertSuccess(adjustOrderLine);
  197. const updatedLine = adjustOrderLine.lines.find(line => line.id === orderLineId);
  198. expect(updatedLine.customFields).toEqual({
  199. stringField: 'initial value', // preserved
  200. intField: 200, // updated
  201. booleanField: true, // updated
  202. nullableField: 'not null', // preserved
  203. relationField: null, // preserved
  204. });
  205. });
  206. it('should allow unsetting fields using null', async () => {
  207. // Create a fresh order line for this test
  208. const { addItemToOrder } = await shopClient.query(addItemToOrderWithCustomFieldsDocument, {
  209. productVariantId: 'T_1',
  210. quantity: 1,
  211. customFields: {
  212. stringField: 'initial value',
  213. intField: 100,
  214. booleanField: false,
  215. nullableField: 'not null',
  216. },
  217. });
  218. orderGuard.assertSuccess(addItemToOrder);
  219. const orderLineId = addItemToOrder.lines[0].id;
  220. const { adjustOrderLine } = await shopClient.query(adjustOrderLineWithCustomFieldsDocument, {
  221. orderLineId,
  222. quantity: 2,
  223. customFields: {
  224. nullableField: null,
  225. },
  226. });
  227. orderGuard.assertSuccess(adjustOrderLine);
  228. const updatedLine = adjustOrderLine.lines.find(line => line.id === orderLineId);
  229. expect(updatedLine.customFields).toEqual({
  230. stringField: 'initial value', // preserved
  231. intField: 100, // preserved
  232. booleanField: false, // preserved
  233. nullableField: null, // unset using null
  234. relationField: null, // preserved
  235. });
  236. });
  237. it('should handle relation field updates with merging', async () => {
  238. // Create a fresh order line for this test
  239. const { addItemToOrder } = await shopClient.query(addItemToOrderWithCustomFieldsDocument, {
  240. productVariantId: 'T_2',
  241. quantity: 1,
  242. customFields: {
  243. stringField: 'initial value',
  244. intField: 100,
  245. booleanField: false,
  246. nullableField: 'not null',
  247. },
  248. });
  249. orderGuard.assertSuccess(addItemToOrder);
  250. const orderLineId = addItemToOrder.lines[0].id;
  251. const { adjustOrderLine } = await shopClient.query(adjustOrderLineWithCustomFieldsDocument, {
  252. orderLineId,
  253. quantity: 2,
  254. customFields: {
  255. relationFieldId: 'T_1',
  256. },
  257. });
  258. orderGuard.assertSuccess(adjustOrderLine);
  259. const updatedLine = adjustOrderLine.lines.find(line => line.id === orderLineId);
  260. expect(updatedLine.customFields).toEqual({
  261. stringField: 'initial value', // preserved
  262. intField: 100, // preserved
  263. booleanField: false, // preserved
  264. nullableField: 'not null', // preserved
  265. relationField: {
  266. id: 'T_1',
  267. name: 'Laptop',
  268. },
  269. });
  270. });
  271. it('should allow unsetting relation field using null', async () => {
  272. // Create a fresh order line for this test
  273. const { addItemToOrder } = await shopClient.query(addItemToOrderWithCustomFieldsDocument, {
  274. productVariantId: 'T_3',
  275. quantity: 1,
  276. customFields: {
  277. stringField: 'initial value',
  278. intField: 100,
  279. booleanField: false,
  280. nullableField: 'not null',
  281. relationFieldId: 'T_1',
  282. },
  283. });
  284. orderGuard.assertSuccess(addItemToOrder);
  285. const orderLineId = addItemToOrder.lines[0].id;
  286. const { adjustOrderLine } = await shopClient.query(adjustOrderLineWithCustomFieldsDocument, {
  287. orderLineId,
  288. quantity: 2,
  289. customFields: {
  290. relationFieldId: null,
  291. },
  292. });
  293. orderGuard.assertSuccess(adjustOrderLine);
  294. const updatedLine = adjustOrderLine.lines.find(line => line.id === orderLineId);
  295. expect(updatedLine.customFields).toEqual({
  296. stringField: 'initial value', // preserved
  297. intField: 100, // preserved
  298. booleanField: false, // preserved
  299. nullableField: 'not null', // preserved
  300. relationField: null, // unset using null
  301. });
  302. });
  303. });
  304. describe('edge cases', () => {
  305. it('should handle empty custom fields object', async () => {
  306. const { addItemToOrder } = await shopClient.query(addItemToOrderWithCustomFieldsDocument, {
  307. productVariantId: 'T_4',
  308. quantity: 1,
  309. customFields: {},
  310. });
  311. orderGuard.assertSuccess(addItemToOrder);
  312. const newLine = addItemToOrder.lines[0];
  313. expect(newLine.customFields).toEqual({
  314. stringField: null,
  315. intField: null,
  316. booleanField: null,
  317. nullableField: null,
  318. relationField: null,
  319. });
  320. });
  321. it('should handle adjustOrderLine with empty custom fields', async () => {
  322. const { addItemToOrder } = await shopClient.query(addItemToOrderWithCustomFieldsDocument, {
  323. productVariantId: 'T_1',
  324. quantity: 1,
  325. customFields: { stringField: 'will be preserved', intField: 999 },
  326. });
  327. orderGuard.assertSuccess(addItemToOrder);
  328. const lineId = addItemToOrder.lines[0].id;
  329. const { adjustOrderLine } = await shopClient.query(adjustOrderLineWithCustomFieldsDocument, {
  330. orderLineId: lineId,
  331. quantity: 2,
  332. customFields: {},
  333. });
  334. orderGuard.assertSuccess(adjustOrderLine);
  335. const updatedLine = adjustOrderLine.lines.find(line => line.id === lineId);
  336. expect(updatedLine.customFields).toEqual({
  337. stringField: 'will be preserved', // preserved when empty object passed
  338. intField: 999, // preserved when empty object passed
  339. booleanField: null, // default value for unset fields
  340. nullableField: null, // default value for unset fields
  341. relationField: null, // default value for unset fields
  342. });
  343. });
  344. });
  345. });