product.e2e-spec.ts 83 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161
  1. import { omit } from '@vendure/common/lib/omit';
  2. import { pick } from '@vendure/common/lib/pick';
  3. import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
  4. import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
  5. import gql from 'graphql-tag';
  6. import path from 'path';
  7. import { initialData } from '../../../e2e-common/e2e-initial-data';
  8. import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
  9. import { PRODUCT_VARIANT_FRAGMENT, PRODUCT_WITH_OPTIONS_FRAGMENT } from './graphql/fragments';
  10. import {
  11. AddOptionGroupToProduct,
  12. ChannelFragment,
  13. CreateProduct,
  14. CreateProductOptionGroupMutation,
  15. CreateProductOptionGroupMutationVariables,
  16. CreateProductVariants,
  17. DeleteProduct,
  18. DeleteProductVariant,
  19. DeletionResult,
  20. ErrorCode,
  21. GetAssetList,
  22. GetOptionGroup,
  23. GetProductList,
  24. GetProductSimple,
  25. GetProductVariant,
  26. GetProductVariantList,
  27. GetProductWithVariantList,
  28. GetProductWithVariants,
  29. GetProductWithVariantsQuery,
  30. GetProductWithVariantsQueryVariables,
  31. LanguageCode,
  32. ProductVariantFragment,
  33. ProductVariantListOptions,
  34. ProductWithOptionsFragment,
  35. ProductWithVariants,
  36. RemoveOptionGroupFromProduct,
  37. SortOrder,
  38. UpdateChannel,
  39. UpdateGlobalSettings,
  40. UpdateProduct,
  41. UpdateProductVariants,
  42. } from './graphql/generated-e2e-admin-types';
  43. import {
  44. ADD_OPTION_GROUP_TO_PRODUCT,
  45. CREATE_PRODUCT,
  46. CREATE_PRODUCT_OPTION_GROUP,
  47. CREATE_PRODUCT_VARIANTS,
  48. DELETE_PRODUCT,
  49. DELETE_PRODUCT_VARIANT,
  50. GET_ASSET_LIST,
  51. GET_PRODUCT_LIST,
  52. GET_PRODUCT_SIMPLE,
  53. GET_PRODUCT_VARIANT_LIST,
  54. GET_PRODUCT_WITH_VARIANTS,
  55. UPDATE_CHANNEL,
  56. UPDATE_GLOBAL_SETTINGS,
  57. UPDATE_PRODUCT,
  58. UPDATE_PRODUCT_VARIANTS,
  59. } from './graphql/shared-definitions';
  60. import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
  61. // tslint:disable:no-non-null-assertion
  62. describe('Product resolver', () => {
  63. const { server, adminClient, shopClient } = createTestEnvironment({
  64. ...testConfig(),
  65. // logger: new DefaultLogger(),
  66. });
  67. const removeOptionGuard: ErrorResultGuard<ProductWithOptionsFragment> = createErrorResultGuard(
  68. input => !!input.optionGroups,
  69. );
  70. const updateChannelGuard: ErrorResultGuard<ChannelFragment> = createErrorResultGuard(input => !!input.id);
  71. beforeAll(async () => {
  72. await server.init({
  73. initialData,
  74. customerCount: 1,
  75. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
  76. });
  77. await adminClient.asSuperAdmin();
  78. }, TEST_SETUP_TIMEOUT_MS);
  79. afterAll(async () => {
  80. await server.destroy();
  81. });
  82. describe('products list query', () => {
  83. it('returns all products when no options passed', async () => {
  84. const result = await adminClient.query<GetProductList.Query, GetProductList.Variables>(
  85. GET_PRODUCT_LIST,
  86. {},
  87. );
  88. expect(result.products.items.length).toBe(20);
  89. expect(result.products.totalItems).toBe(20);
  90. });
  91. it('limits result set with skip & take', async () => {
  92. const result = await adminClient.query<GetProductList.Query, GetProductList.Variables>(
  93. GET_PRODUCT_LIST,
  94. {
  95. options: {
  96. skip: 0,
  97. take: 3,
  98. },
  99. },
  100. );
  101. expect(result.products.items.length).toBe(3);
  102. expect(result.products.totalItems).toBe(20);
  103. });
  104. it('filters by name admin', async () => {
  105. const result = await adminClient.query<GetProductList.Query, GetProductList.Variables>(
  106. GET_PRODUCT_LIST,
  107. {
  108. options: {
  109. filter: {
  110. name: {
  111. contains: 'skateboard',
  112. },
  113. },
  114. },
  115. },
  116. );
  117. expect(result.products.items.length).toBe(1);
  118. expect(result.products.items[0].name).toBe('Cruiser Skateboard');
  119. });
  120. it('filters multiple admin', async () => {
  121. const result = await adminClient.query<GetProductList.Query, GetProductList.Variables>(
  122. GET_PRODUCT_LIST,
  123. {
  124. options: {
  125. filter: {
  126. name: {
  127. contains: 'camera',
  128. },
  129. slug: {
  130. contains: 'tent',
  131. },
  132. },
  133. },
  134. },
  135. );
  136. expect(result.products.items.length).toBe(0);
  137. });
  138. it('sorts by name admin', async () => {
  139. const result = await adminClient.query<GetProductList.Query, GetProductList.Variables>(
  140. GET_PRODUCT_LIST,
  141. {
  142. options: {
  143. sort: {
  144. name: SortOrder.ASC,
  145. },
  146. },
  147. },
  148. );
  149. expect(result.products.items.map(p => p.name)).toEqual([
  150. 'Bonsai Tree',
  151. 'Boxing Gloves',
  152. 'Camera Lens',
  153. 'Clacky Keyboard',
  154. 'Cruiser Skateboard',
  155. 'Curvy Monitor',
  156. 'Football',
  157. 'Gaming PC',
  158. 'Hard Drive',
  159. 'Instant Camera',
  160. 'Laptop',
  161. 'Orchid',
  162. 'Road Bike',
  163. 'Running Shoe',
  164. 'Skipping Rope',
  165. 'Slr Camera',
  166. 'Spiky Cactus',
  167. 'Tent',
  168. 'Tripod',
  169. 'USB Cable',
  170. ]);
  171. });
  172. it('filters by name shop', async () => {
  173. const result = await shopClient.query<GetProductList.Query, GetProductList.Variables>(
  174. GET_PRODUCT_LIST,
  175. {
  176. options: {
  177. filter: {
  178. name: {
  179. contains: 'skateboard',
  180. },
  181. },
  182. },
  183. },
  184. );
  185. expect(result.products.items.length).toBe(1);
  186. expect(result.products.items[0].name).toBe('Cruiser Skateboard');
  187. });
  188. it('sorts by name shop', async () => {
  189. const result = await shopClient.query<GetProductList.Query, GetProductList.Variables>(
  190. GET_PRODUCT_LIST,
  191. {
  192. options: {
  193. sort: {
  194. name: SortOrder.ASC,
  195. },
  196. },
  197. },
  198. );
  199. expect(result.products.items.map(p => p.name)).toEqual([
  200. 'Bonsai Tree',
  201. 'Boxing Gloves',
  202. 'Camera Lens',
  203. 'Clacky Keyboard',
  204. 'Cruiser Skateboard',
  205. 'Curvy Monitor',
  206. 'Football',
  207. 'Gaming PC',
  208. 'Hard Drive',
  209. 'Instant Camera',
  210. 'Laptop',
  211. 'Orchid',
  212. 'Road Bike',
  213. 'Running Shoe',
  214. 'Skipping Rope',
  215. 'Slr Camera',
  216. 'Spiky Cactus',
  217. 'Tent',
  218. 'Tripod',
  219. 'USB Cable',
  220. ]);
  221. });
  222. });
  223. describe('product query', () => {
  224. it('by id', async () => {
  225. const { product } = await adminClient.query<GetProductSimple.Query, GetProductSimple.Variables>(
  226. GET_PRODUCT_SIMPLE,
  227. { id: 'T_2' },
  228. );
  229. if (!product) {
  230. fail('Product not found');
  231. return;
  232. }
  233. expect(product.id).toBe('T_2');
  234. });
  235. it('by slug', async () => {
  236. const { product } = await adminClient.query<GetProductSimple.Query, GetProductSimple.Variables>(
  237. GET_PRODUCT_SIMPLE,
  238. { slug: 'curvy-monitor' },
  239. );
  240. if (!product) {
  241. fail('Product not found');
  242. return;
  243. }
  244. expect(product.slug).toBe('curvy-monitor');
  245. });
  246. // https://github.com/vendure-ecommerce/vendure/issues/820
  247. it('by slug with multiple assets', async () => {
  248. const { product: product1 } = await adminClient.query<
  249. GetProductSimple.Query,
  250. GetProductSimple.Variables
  251. >(GET_PRODUCT_SIMPLE, { id: 'T_1' });
  252. const result = await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(
  253. UPDATE_PRODUCT,
  254. {
  255. input: {
  256. id: product1!.id,
  257. assetIds: ['T_1', 'T_2', 'T_3'],
  258. },
  259. },
  260. );
  261. const { product } = await adminClient.query<
  262. GetProductWithVariants.Query,
  263. GetProductWithVariants.Variables
  264. >(GET_PRODUCT_WITH_VARIANTS, { slug: product1!.slug });
  265. if (!product) {
  266. fail('Product not found');
  267. return;
  268. }
  269. expect(product.assets.map(a => a.id)).toEqual(['T_1', 'T_2', 'T_3']);
  270. });
  271. // https://github.com/vendure-ecommerce/vendure/issues/538
  272. it('falls back to default language slug', async () => {
  273. const { product } = await adminClient.query<GetProductSimple.Query, GetProductSimple.Variables>(
  274. GET_PRODUCT_SIMPLE,
  275. { slug: 'curvy-monitor' },
  276. { languageCode: LanguageCode.de },
  277. );
  278. if (!product) {
  279. fail('Product not found');
  280. return;
  281. }
  282. expect(product.slug).toBe('curvy-monitor');
  283. });
  284. it(
  285. 'throws if neither id nor slug provided',
  286. assertThrowsWithMessage(async () => {
  287. await adminClient.query<GetProductSimple.Query, GetProductSimple.Variables>(
  288. GET_PRODUCT_SIMPLE,
  289. {},
  290. );
  291. }, 'Either the Product id or slug must be provided'),
  292. );
  293. it(
  294. 'throws if id and slug do not refer to the same Product',
  295. assertThrowsWithMessage(async () => {
  296. await adminClient.query<GetProductSimple.Query, GetProductSimple.Variables>(
  297. GET_PRODUCT_SIMPLE,
  298. {
  299. id: 'T_2',
  300. slug: 'laptop',
  301. },
  302. );
  303. }, 'The provided id and slug refer to different Products'),
  304. );
  305. it('returns expected properties', async () => {
  306. const { product } = await adminClient.query<
  307. GetProductWithVariants.Query,
  308. GetProductWithVariants.Variables
  309. >(GET_PRODUCT_WITH_VARIANTS, {
  310. id: 'T_2',
  311. });
  312. if (!product) {
  313. fail('Product not found');
  314. return;
  315. }
  316. expect(omit(product, ['variants'])).toMatchSnapshot();
  317. expect(product.variants.length).toBe(2);
  318. });
  319. it('ProductVariant price properties are correct', async () => {
  320. const result = await adminClient.query<
  321. GetProductWithVariants.Query,
  322. GetProductWithVariants.Variables
  323. >(GET_PRODUCT_WITH_VARIANTS, {
  324. id: 'T_2',
  325. });
  326. if (!result.product) {
  327. fail('Product not found');
  328. return;
  329. }
  330. expect(result.product.variants[0].price).toBe(14374);
  331. expect(result.product.variants[0].taxCategory).toEqual({
  332. id: 'T_1',
  333. name: 'Standard Tax',
  334. });
  335. });
  336. it('returns null when id not found', async () => {
  337. const result = await adminClient.query<
  338. GetProductWithVariants.Query,
  339. GetProductWithVariants.Variables
  340. >(GET_PRODUCT_WITH_VARIANTS, {
  341. id: 'bad_id',
  342. });
  343. expect(result.product).toBeNull();
  344. });
  345. it('returns null when slug not found', async () => {
  346. const result = await adminClient.query<
  347. GetProductWithVariants.Query,
  348. GetProductWithVariants.Variables
  349. >(GET_PRODUCT_WITH_VARIANTS, {
  350. slug: 'bad_slug',
  351. });
  352. expect(result.product).toBeNull();
  353. });
  354. describe('product query with translations', () => {
  355. let translatedProduct: ProductWithVariants.Fragment;
  356. let en_translation: ProductWithVariants.Translations;
  357. let de_translation: ProductWithVariants.Translations;
  358. beforeAll(async () => {
  359. const result = await adminClient.query<CreateProduct.Mutation, CreateProduct.Variables>(
  360. CREATE_PRODUCT,
  361. {
  362. input: {
  363. translations: [
  364. {
  365. languageCode: LanguageCode.en,
  366. name: 'en Pineapple',
  367. slug: 'en-pineapple',
  368. description: 'A delicious pineapple',
  369. },
  370. {
  371. languageCode: LanguageCode.de,
  372. name: 'de Ananas',
  373. slug: 'de-ananas',
  374. description: 'Eine köstliche Ananas',
  375. },
  376. ],
  377. },
  378. },
  379. );
  380. translatedProduct = result.createProduct;
  381. en_translation = translatedProduct.translations.find(
  382. t => t.languageCode === LanguageCode.en,
  383. )!;
  384. de_translation = translatedProduct.translations.find(
  385. t => t.languageCode === LanguageCode.de,
  386. )!;
  387. });
  388. it('en slug without translation arg', async () => {
  389. const { product } = await adminClient.query<
  390. GetProductSimple.Query,
  391. GetProductSimple.Variables
  392. >(GET_PRODUCT_SIMPLE, { slug: en_translation.slug });
  393. if (!product) {
  394. fail('Product not found');
  395. return;
  396. }
  397. expect(product.slug).toBe(en_translation.slug);
  398. });
  399. it('de slug without translation arg', async () => {
  400. const { product } = await adminClient.query<
  401. GetProductSimple.Query,
  402. GetProductSimple.Variables
  403. >(GET_PRODUCT_SIMPLE, { slug: de_translation.slug });
  404. if (!product) {
  405. fail('Product not found');
  406. return;
  407. }
  408. expect(product.slug).toBe(en_translation.slug);
  409. });
  410. it('en slug with translation en', async () => {
  411. const { product } = await adminClient.query<
  412. GetProductSimple.Query,
  413. GetProductSimple.Variables
  414. >(GET_PRODUCT_SIMPLE, { slug: en_translation.slug }, { languageCode: LanguageCode.en });
  415. if (!product) {
  416. fail('Product not found');
  417. return;
  418. }
  419. expect(product.slug).toBe(en_translation.slug);
  420. });
  421. it('de slug with translation en', async () => {
  422. const { product } = await adminClient.query<
  423. GetProductSimple.Query,
  424. GetProductSimple.Variables
  425. >(GET_PRODUCT_SIMPLE, { slug: de_translation.slug }, { languageCode: LanguageCode.en });
  426. if (!product) {
  427. fail('Product not found');
  428. return;
  429. }
  430. expect(product.slug).toBe(en_translation.slug);
  431. });
  432. it('en slug with translation de', async () => {
  433. const { product } = await adminClient.query<
  434. GetProductSimple.Query,
  435. GetProductSimple.Variables
  436. >(GET_PRODUCT_SIMPLE, { slug: en_translation.slug }, { languageCode: LanguageCode.de });
  437. if (!product) {
  438. fail('Product not found');
  439. return;
  440. }
  441. expect(product.slug).toBe(de_translation.slug);
  442. });
  443. it('de slug with translation de', async () => {
  444. const { product } = await adminClient.query<
  445. GetProductSimple.Query,
  446. GetProductSimple.Variables
  447. >(GET_PRODUCT_SIMPLE, { slug: de_translation.slug }, { languageCode: LanguageCode.de });
  448. if (!product) {
  449. fail('Product not found');
  450. return;
  451. }
  452. expect(product.slug).toBe(de_translation.slug);
  453. });
  454. it('de slug with translation ru', async () => {
  455. const { product } = await adminClient.query<
  456. GetProductSimple.Query,
  457. GetProductSimple.Variables
  458. >(GET_PRODUCT_SIMPLE, { slug: de_translation.slug }, { languageCode: LanguageCode.ru });
  459. if (!product) {
  460. fail('Product not found');
  461. return;
  462. }
  463. expect(product.slug).toBe(en_translation.slug);
  464. });
  465. });
  466. describe('product.variants', () => {
  467. it('returns product variants', async () => {
  468. const { product } = await adminClient.query<
  469. GetProductWithVariants.Query,
  470. GetProductWithVariants.Variables
  471. >(GET_PRODUCT_WITH_VARIANTS, {
  472. id: 'T_1',
  473. });
  474. expect(product?.variants.length).toBe(4);
  475. });
  476. it('returns product variants in existing language', async () => {
  477. const { product } = await adminClient.query<
  478. GetProductWithVariants.Query,
  479. GetProductWithVariants.Variables
  480. >(
  481. GET_PRODUCT_WITH_VARIANTS,
  482. {
  483. id: 'T_1',
  484. },
  485. { languageCode: LanguageCode.en },
  486. );
  487. expect(product?.variants.length).toBe(4);
  488. });
  489. it('returns product variants in non-existing language', async () => {
  490. const { product } = await adminClient.query<
  491. GetProductWithVariants.Query,
  492. GetProductWithVariants.Variables
  493. >(
  494. GET_PRODUCT_WITH_VARIANTS,
  495. {
  496. id: 'T_1',
  497. },
  498. { languageCode: LanguageCode.ru },
  499. );
  500. expect(product?.variants.length).toBe(4);
  501. });
  502. });
  503. describe('product.variants', () => {
  504. it('returns product variants', async () => {
  505. const { product } = await adminClient.query<
  506. GetProductWithVariants.Query,
  507. GetProductWithVariants.Variables
  508. >(GET_PRODUCT_WITH_VARIANTS, {
  509. id: 'T_1',
  510. });
  511. expect(product?.variants.length).toBe(4);
  512. });
  513. it('returns product variants in existing language', async () => {
  514. const { product } = await adminClient.query<
  515. GetProductWithVariants.Query,
  516. GetProductWithVariants.Variables
  517. >(
  518. GET_PRODUCT_WITH_VARIANTS,
  519. {
  520. id: 'T_1',
  521. },
  522. { languageCode: LanguageCode.en },
  523. );
  524. expect(product?.variants.length).toBe(4);
  525. });
  526. it('returns product variants in non-existing language', async () => {
  527. const { product } = await adminClient.query<
  528. GetProductWithVariants.Query,
  529. GetProductWithVariants.Variables
  530. >(
  531. GET_PRODUCT_WITH_VARIANTS,
  532. {
  533. id: 'T_1',
  534. },
  535. { languageCode: LanguageCode.ru },
  536. );
  537. expect(product?.variants.length).toBe(4);
  538. });
  539. });
  540. describe('product.variantList', () => {
  541. it('returns product variants', async () => {
  542. const { product } = await adminClient.query<
  543. GetProductWithVariantList.Query,
  544. GetProductWithVariantList.Variables
  545. >(GET_PRODUCT_WITH_VARIANT_LIST, {
  546. id: 'T_1',
  547. });
  548. expect(product?.variantList.items.length).toBe(4);
  549. expect(product?.variantList.totalItems).toBe(4);
  550. });
  551. it('returns product variants in existing language', async () => {
  552. const { product } = await adminClient.query<
  553. GetProductWithVariantList.Query,
  554. GetProductWithVariantList.Variables
  555. >(
  556. GET_PRODUCT_WITH_VARIANT_LIST,
  557. {
  558. id: 'T_1',
  559. },
  560. { languageCode: LanguageCode.en },
  561. );
  562. expect(product?.variantList.items.length).toBe(4);
  563. });
  564. it('returns product variants in non-existing language', async () => {
  565. const { product } = await adminClient.query<
  566. GetProductWithVariantList.Query,
  567. GetProductWithVariantList.Variables
  568. >(
  569. GET_PRODUCT_WITH_VARIANT_LIST,
  570. {
  571. id: 'T_1',
  572. },
  573. { languageCode: LanguageCode.ru },
  574. );
  575. expect(product?.variantList.items.length).toBe(4);
  576. });
  577. it('filter & sort', async () => {
  578. const { product } = await adminClient.query<
  579. GetProductWithVariantList.Query,
  580. GetProductWithVariantList.Variables
  581. >(GET_PRODUCT_WITH_VARIANT_LIST, {
  582. id: 'T_1',
  583. variantListOptions: {
  584. filter: {
  585. name: {
  586. contains: '15',
  587. },
  588. },
  589. sort: {
  590. price: SortOrder.DESC,
  591. },
  592. },
  593. });
  594. expect(product?.variantList.items.map(i => i.name)).toEqual([
  595. 'Laptop 15 inch 16GB',
  596. 'Laptop 15 inch 8GB',
  597. ]);
  598. });
  599. });
  600. });
  601. describe('productVariants list query', () => {
  602. it('returns list', async () => {
  603. const { productVariants } = await adminClient.query<
  604. GetProductVariantList.Query,
  605. GetProductVariantList.Variables
  606. >(GET_PRODUCT_VARIANT_LIST, {
  607. options: {
  608. take: 3,
  609. sort: {
  610. name: SortOrder.ASC,
  611. },
  612. },
  613. });
  614. expect(productVariants.items).toEqual([
  615. {
  616. id: 'T_34',
  617. name: 'Bonsai Tree',
  618. price: 1999,
  619. priceWithTax: 2399,
  620. sku: 'B01MXFLUSV',
  621. },
  622. {
  623. id: 'T_24',
  624. name: 'Boxing Gloves',
  625. price: 3304,
  626. priceWithTax: 3965,
  627. sku: 'B000ZYLPPU',
  628. },
  629. {
  630. id: 'T_19',
  631. name: 'Camera Lens',
  632. price: 10400,
  633. priceWithTax: 12480,
  634. sku: 'B0012UUP02',
  635. },
  636. ]);
  637. });
  638. it('sort by price', async () => {
  639. const { productVariants } = await adminClient.query<
  640. GetProductVariantList.Query,
  641. GetProductVariantList.Variables
  642. >(GET_PRODUCT_VARIANT_LIST, {
  643. options: {
  644. take: 3,
  645. sort: {
  646. price: SortOrder.ASC,
  647. },
  648. },
  649. });
  650. expect(productVariants.items).toEqual([
  651. {
  652. id: 'T_23',
  653. name: 'Skipping Rope',
  654. price: 799,
  655. priceWithTax: 959,
  656. sku: 'B07CNGXVXT',
  657. },
  658. {
  659. id: 'T_20',
  660. name: 'Tripod',
  661. price: 1498,
  662. priceWithTax: 1798,
  663. sku: 'B00XI87KV8',
  664. },
  665. {
  666. id: 'T_32',
  667. name: 'Spiky Cactus',
  668. price: 1550,
  669. priceWithTax: 1860,
  670. sku: 'SC011001',
  671. },
  672. ]);
  673. });
  674. it('sort by priceWithTax', async () => {
  675. const { productVariants } = await adminClient.query<
  676. GetProductVariantList.Query,
  677. GetProductVariantList.Variables
  678. >(GET_PRODUCT_VARIANT_LIST, {
  679. options: {
  680. take: 3,
  681. sort: {
  682. priceWithTax: SortOrder.ASC,
  683. },
  684. },
  685. });
  686. expect(productVariants.items).toEqual([
  687. {
  688. id: 'T_23',
  689. name: 'Skipping Rope',
  690. price: 799,
  691. priceWithTax: 959,
  692. sku: 'B07CNGXVXT',
  693. },
  694. {
  695. id: 'T_20',
  696. name: 'Tripod',
  697. price: 1498,
  698. priceWithTax: 1798,
  699. sku: 'B00XI87KV8',
  700. },
  701. {
  702. id: 'T_32',
  703. name: 'Spiky Cactus',
  704. price: 1550,
  705. priceWithTax: 1860,
  706. sku: 'SC011001',
  707. },
  708. ]);
  709. });
  710. it('filter by price', async () => {
  711. const { productVariants } = await adminClient.query<
  712. GetProductVariantList.Query,
  713. GetProductVariantList.Variables
  714. >(GET_PRODUCT_VARIANT_LIST, {
  715. options: {
  716. take: 3,
  717. filter: {
  718. price: {
  719. between: {
  720. start: 1400,
  721. end: 1500,
  722. },
  723. },
  724. },
  725. },
  726. });
  727. expect(productVariants.items).toEqual([
  728. {
  729. id: 'T_20',
  730. name: 'Tripod',
  731. price: 1498,
  732. priceWithTax: 1798,
  733. sku: 'B00XI87KV8',
  734. },
  735. ]);
  736. });
  737. it('filter by priceWithTax', async () => {
  738. const { productVariants } = await adminClient.query<
  739. GetProductVariantList.Query,
  740. GetProductVariantList.Variables
  741. >(GET_PRODUCT_VARIANT_LIST, {
  742. options: {
  743. take: 3,
  744. filter: {
  745. priceWithTax: {
  746. between: {
  747. start: 1400,
  748. end: 1500,
  749. },
  750. },
  751. },
  752. },
  753. });
  754. // Note the results are incorrect. This is a design trade-off. See the
  755. // commend on the ProductVariant.priceWithTax annotation for explanation.
  756. expect(productVariants.items).toEqual([
  757. {
  758. id: 'T_20',
  759. name: 'Tripod',
  760. price: 1498,
  761. priceWithTax: 1798,
  762. sku: 'B00XI87KV8',
  763. },
  764. ]);
  765. });
  766. it('returns variants for particular product by id', async () => {
  767. const { productVariants } = await adminClient.query<
  768. GetProductVariantList.Query,
  769. GetProductVariantList.Variables
  770. >(GET_PRODUCT_VARIANT_LIST, {
  771. options: {
  772. take: 3,
  773. sort: {
  774. price: SortOrder.ASC,
  775. },
  776. },
  777. productId: 'T_1',
  778. });
  779. expect(productVariants.items).toEqual([
  780. {
  781. id: 'T_1',
  782. name: 'Laptop 13 inch 8GB',
  783. price: 129900,
  784. priceWithTax: 155880,
  785. sku: 'L2201308',
  786. },
  787. {
  788. id: 'T_2',
  789. name: 'Laptop 15 inch 8GB',
  790. price: 139900,
  791. priceWithTax: 167880,
  792. sku: 'L2201508',
  793. },
  794. {
  795. id: 'T_3',
  796. name: 'Laptop 13 inch 16GB',
  797. priceWithTax: 263880,
  798. price: 219900,
  799. sku: 'L2201316',
  800. },
  801. ]);
  802. });
  803. });
  804. describe('productVariant query', () => {
  805. it('by id', async () => {
  806. const { productVariant } = await adminClient.query<
  807. GetProductVariant.Query,
  808. GetProductVariant.Variables
  809. >(GET_PRODUCT_VARIANT, {
  810. id: 'T_1',
  811. });
  812. expect(productVariant?.id).toBe('T_1');
  813. expect(productVariant?.name).toBe('Laptop 13 inch 8GB');
  814. });
  815. it('returns null when id not found', async () => {
  816. const { productVariant } = await adminClient.query<
  817. GetProductVariant.Query,
  818. GetProductVariant.Variables
  819. >(GET_PRODUCT_VARIANT, {
  820. id: 'T_999',
  821. });
  822. expect(productVariant).toBeNull();
  823. });
  824. });
  825. describe('product mutation', () => {
  826. let newTranslatedProduct: ProductWithVariants.Fragment;
  827. let newProduct: ProductWithVariants.Fragment;
  828. let newProductWithAssets: ProductWithVariants.Fragment;
  829. it('createProduct creates a new Product', async () => {
  830. const result = await adminClient.query<CreateProduct.Mutation, CreateProduct.Variables>(
  831. CREATE_PRODUCT,
  832. {
  833. input: {
  834. translations: [
  835. {
  836. languageCode: LanguageCode.en,
  837. name: 'en Baked Potato',
  838. slug: 'en Baked Potato',
  839. description: 'A baked potato',
  840. },
  841. {
  842. languageCode: LanguageCode.de,
  843. name: 'de Baked Potato',
  844. slug: 'de-baked-potato',
  845. description: 'Eine baked Erdapfel',
  846. },
  847. ],
  848. },
  849. },
  850. );
  851. expect(omit(result.createProduct, ['translations'])).toMatchSnapshot();
  852. expect(result.createProduct.translations.map(t => t.description).sort()).toEqual([
  853. 'A baked potato',
  854. 'Eine baked Erdapfel',
  855. ]);
  856. newTranslatedProduct = result.createProduct;
  857. });
  858. it('createProduct creates a new Product with assets', async () => {
  859. const assetsResult = await adminClient.query<GetAssetList.Query, GetAssetList.Variables>(
  860. GET_ASSET_LIST,
  861. );
  862. const assetIds = assetsResult.assets.items.slice(0, 2).map(a => a.id);
  863. const featuredAssetId = assetsResult.assets.items[0].id;
  864. const result = await adminClient.query<CreateProduct.Mutation, CreateProduct.Variables>(
  865. CREATE_PRODUCT,
  866. {
  867. input: {
  868. assetIds,
  869. featuredAssetId,
  870. translations: [
  871. {
  872. languageCode: LanguageCode.en,
  873. name: 'en Has Assets',
  874. slug: 'en-has-assets',
  875. description: 'A product with assets',
  876. },
  877. ],
  878. },
  879. },
  880. );
  881. expect(result.createProduct.assets.map(a => a.id)).toEqual(assetIds);
  882. expect(result.createProduct.featuredAsset!.id).toBe(featuredAssetId);
  883. newProductWithAssets = result.createProduct;
  884. });
  885. it('createProduct creates a disabled Product', async () => {
  886. const result = await adminClient.query<CreateProduct.Mutation, CreateProduct.Variables>(
  887. CREATE_PRODUCT,
  888. {
  889. input: {
  890. enabled: false,
  891. translations: [
  892. {
  893. languageCode: LanguageCode.en,
  894. name: 'en Small apple',
  895. slug: 'en-small-apple',
  896. description: 'A small apple',
  897. },
  898. ],
  899. },
  900. },
  901. );
  902. expect(result.createProduct.enabled).toBe(false);
  903. newProduct = result.createProduct;
  904. });
  905. it('updateProduct updates a Product', async () => {
  906. const result = await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(
  907. UPDATE_PRODUCT,
  908. {
  909. input: {
  910. id: newProduct.id,
  911. translations: [
  912. {
  913. languageCode: LanguageCode.en,
  914. name: 'en Mashed Potato',
  915. slug: 'en-mashed-potato',
  916. description: 'A blob of mashed potato',
  917. },
  918. {
  919. languageCode: LanguageCode.de,
  920. name: 'de Mashed Potato',
  921. slug: 'de-mashed-potato',
  922. description: 'Eine blob von gemashed Erdapfel',
  923. },
  924. ],
  925. },
  926. },
  927. );
  928. expect(result.updateProduct.translations.map(t => t.description).sort()).toEqual([
  929. 'A blob of mashed potato',
  930. 'Eine blob von gemashed Erdapfel',
  931. ]);
  932. });
  933. it('slug is normalized to be url-safe', async () => {
  934. const result = await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(
  935. UPDATE_PRODUCT,
  936. {
  937. input: {
  938. id: newProduct.id,
  939. translations: [
  940. {
  941. languageCode: LanguageCode.en,
  942. name: 'en Mashed Potato',
  943. slug: 'A (very) nice potato!!',
  944. description: 'A blob of mashed potato',
  945. },
  946. ],
  947. },
  948. },
  949. );
  950. expect(result.updateProduct.slug).toBe('a-very-nice-potato');
  951. });
  952. it('create with duplicate slug is renamed to be unique', async () => {
  953. const result = await adminClient.query<CreateProduct.Mutation, CreateProduct.Variables>(
  954. CREATE_PRODUCT,
  955. {
  956. input: {
  957. translations: [
  958. {
  959. languageCode: LanguageCode.en,
  960. name: 'Another baked potato',
  961. slug: 'a-very-nice-potato',
  962. description: 'Another baked potato but a bit different',
  963. },
  964. ],
  965. },
  966. },
  967. );
  968. expect(result.createProduct.slug).toBe('a-very-nice-potato-2');
  969. });
  970. it('update with duplicate slug is renamed to be unique', async () => {
  971. const result = await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(
  972. UPDATE_PRODUCT,
  973. {
  974. input: {
  975. id: newProduct.id,
  976. translations: [
  977. {
  978. languageCode: LanguageCode.en,
  979. name: 'Yet another baked potato',
  980. slug: 'a-very-nice-potato-2',
  981. description: 'Possibly the final baked potato',
  982. },
  983. ],
  984. },
  985. },
  986. );
  987. expect(result.updateProduct.slug).toBe('a-very-nice-potato-3');
  988. });
  989. it('slug duplicate check does not include self', async () => {
  990. const result = await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(
  991. UPDATE_PRODUCT,
  992. {
  993. input: {
  994. id: newProduct.id,
  995. translations: [
  996. {
  997. languageCode: LanguageCode.en,
  998. slug: 'a-very-nice-potato-3',
  999. },
  1000. ],
  1001. },
  1002. },
  1003. );
  1004. expect(result.updateProduct.slug).toBe('a-very-nice-potato-3');
  1005. });
  1006. it('updateProduct accepts partial input', async () => {
  1007. const result = await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(
  1008. UPDATE_PRODUCT,
  1009. {
  1010. input: {
  1011. id: newProduct.id,
  1012. translations: [
  1013. {
  1014. languageCode: LanguageCode.en,
  1015. name: 'en Very Mashed Potato',
  1016. },
  1017. ],
  1018. },
  1019. },
  1020. );
  1021. expect(result.updateProduct.translations.length).toBe(2);
  1022. expect(
  1023. result.updateProduct.translations.find(t => t.languageCode === LanguageCode.de)!.name,
  1024. ).toBe('de Mashed Potato');
  1025. expect(
  1026. result.updateProduct.translations.find(t => t.languageCode === LanguageCode.en)!.name,
  1027. ).toBe('en Very Mashed Potato');
  1028. expect(
  1029. result.updateProduct.translations.find(t => t.languageCode === LanguageCode.en)!.description,
  1030. ).toBe('Possibly the final baked potato');
  1031. });
  1032. it('updateProduct adds Assets to a product and sets featured asset', async () => {
  1033. const assetsResult = await adminClient.query<GetAssetList.Query, GetAssetList.Variables>(
  1034. GET_ASSET_LIST,
  1035. );
  1036. const assetIds = assetsResult.assets.items.map(a => a.id);
  1037. const featuredAssetId = assetsResult.assets.items[2].id;
  1038. const result = await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(
  1039. UPDATE_PRODUCT,
  1040. {
  1041. input: {
  1042. id: newProduct.id,
  1043. assetIds,
  1044. featuredAssetId,
  1045. },
  1046. },
  1047. );
  1048. expect(result.updateProduct.assets.map(a => a.id)).toEqual(assetIds);
  1049. expect(result.updateProduct.featuredAsset!.id).toBe(featuredAssetId);
  1050. });
  1051. it('updateProduct sets a featured asset', async () => {
  1052. const productResult = await adminClient.query<
  1053. GetProductWithVariants.Query,
  1054. GetProductWithVariants.Variables
  1055. >(GET_PRODUCT_WITH_VARIANTS, {
  1056. id: newProduct.id,
  1057. });
  1058. const assets = productResult.product!.assets;
  1059. const result = await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(
  1060. UPDATE_PRODUCT,
  1061. {
  1062. input: {
  1063. id: newProduct.id,
  1064. featuredAssetId: assets[0].id,
  1065. },
  1066. },
  1067. );
  1068. expect(result.updateProduct.featuredAsset!.id).toBe(assets[0].id);
  1069. });
  1070. it('updateProduct updates assets', async () => {
  1071. const result = await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(
  1072. UPDATE_PRODUCT,
  1073. {
  1074. input: {
  1075. id: newProduct.id,
  1076. featuredAssetId: 'T_1',
  1077. assetIds: ['T_1', 'T_2'],
  1078. },
  1079. },
  1080. );
  1081. expect(result.updateProduct.assets.map(a => a.id)).toEqual(['T_1', 'T_2']);
  1082. });
  1083. it('updateProduct updates FacetValues', async () => {
  1084. const result = await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(
  1085. UPDATE_PRODUCT,
  1086. {
  1087. input: {
  1088. id: newProduct.id,
  1089. facetValueIds: ['T_1'],
  1090. },
  1091. },
  1092. );
  1093. expect(result.updateProduct.facetValues.length).toEqual(1);
  1094. });
  1095. it(
  1096. 'updateProduct errors with an invalid productId',
  1097. assertThrowsWithMessage(
  1098. () =>
  1099. adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
  1100. input: {
  1101. id: '999',
  1102. translations: [
  1103. {
  1104. languageCode: LanguageCode.en,
  1105. name: 'en Mashed Potato',
  1106. slug: 'en-mashed-potato',
  1107. description: 'A blob of mashed potato',
  1108. },
  1109. {
  1110. languageCode: LanguageCode.de,
  1111. name: 'de Mashed Potato',
  1112. slug: 'de-mashed-potato',
  1113. description: 'Eine blob von gemashed Erdapfel',
  1114. },
  1115. ],
  1116. },
  1117. }),
  1118. `No Product with the id '999' could be found`,
  1119. ),
  1120. );
  1121. it('addOptionGroupToProduct adds an option group', async () => {
  1122. const optionGroup = await createOptionGroup('Quark-type', ['Charm', 'Strange']);
  1123. const result = await adminClient.query<
  1124. AddOptionGroupToProduct.Mutation,
  1125. AddOptionGroupToProduct.Variables
  1126. >(ADD_OPTION_GROUP_TO_PRODUCT, {
  1127. optionGroupId: optionGroup.id,
  1128. productId: newProduct.id,
  1129. });
  1130. expect(result.addOptionGroupToProduct.optionGroups.length).toBe(1);
  1131. expect(result.addOptionGroupToProduct.optionGroups[0].id).toBe(optionGroup.id);
  1132. // not really testing this, but just cleaning up for later tests
  1133. const { removeOptionGroupFromProduct } = await adminClient.query<
  1134. RemoveOptionGroupFromProduct.Mutation,
  1135. RemoveOptionGroupFromProduct.Variables
  1136. >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
  1137. optionGroupId: optionGroup.id,
  1138. productId: newProduct.id,
  1139. });
  1140. removeOptionGuard.assertSuccess(removeOptionGroupFromProduct);
  1141. });
  1142. it(
  1143. 'addOptionGroupToProduct errors with an invalid productId',
  1144. assertThrowsWithMessage(
  1145. () =>
  1146. adminClient.query<AddOptionGroupToProduct.Mutation, AddOptionGroupToProduct.Variables>(
  1147. ADD_OPTION_GROUP_TO_PRODUCT,
  1148. {
  1149. optionGroupId: 'T_1',
  1150. productId: 'T_999',
  1151. },
  1152. ),
  1153. `No Product with the id '999' could be found`,
  1154. ),
  1155. );
  1156. it(
  1157. 'addOptionGroupToProduct errors if the OptionGroup is already assigned to another Product',
  1158. assertThrowsWithMessage(
  1159. () =>
  1160. adminClient.query<AddOptionGroupToProduct.Mutation, AddOptionGroupToProduct.Variables>(
  1161. ADD_OPTION_GROUP_TO_PRODUCT,
  1162. {
  1163. optionGroupId: 'T_1',
  1164. productId: 'T_2',
  1165. },
  1166. ),
  1167. `The ProductOptionGroup "laptop-screen-size" is already assigned to the Product "Laptop"`,
  1168. ),
  1169. );
  1170. it(
  1171. 'addOptionGroupToProduct errors with an invalid optionGroupId',
  1172. assertThrowsWithMessage(
  1173. () =>
  1174. adminClient.query<AddOptionGroupToProduct.Mutation, AddOptionGroupToProduct.Variables>(
  1175. ADD_OPTION_GROUP_TO_PRODUCT,
  1176. {
  1177. optionGroupId: '999',
  1178. productId: newProduct.id,
  1179. },
  1180. ),
  1181. `No ProductOptionGroup with the id '999' could be found`,
  1182. ),
  1183. );
  1184. it('removeOptionGroupFromProduct removes an option group', async () => {
  1185. const optionGroup = await createOptionGroup('Length', ['Short', 'Long']);
  1186. const { addOptionGroupToProduct } = await adminClient.query<
  1187. AddOptionGroupToProduct.Mutation,
  1188. AddOptionGroupToProduct.Variables
  1189. >(ADD_OPTION_GROUP_TO_PRODUCT, {
  1190. optionGroupId: optionGroup.id,
  1191. productId: newProductWithAssets.id,
  1192. });
  1193. expect(addOptionGroupToProduct.optionGroups.length).toBe(1);
  1194. const { removeOptionGroupFromProduct } = await adminClient.query<
  1195. RemoveOptionGroupFromProduct.Mutation,
  1196. RemoveOptionGroupFromProduct.Variables
  1197. >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
  1198. optionGroupId: optionGroup.id,
  1199. productId: newProductWithAssets.id,
  1200. });
  1201. removeOptionGuard.assertSuccess(removeOptionGroupFromProduct);
  1202. expect(removeOptionGroupFromProduct.id).toBe(newProductWithAssets.id);
  1203. expect(removeOptionGroupFromProduct.optionGroups.length).toBe(0);
  1204. });
  1205. it('removeOptionGroupFromProduct return error result if the optionGroup is being used by variants', async () => {
  1206. const { removeOptionGroupFromProduct } = await adminClient.query<
  1207. RemoveOptionGroupFromProduct.Mutation,
  1208. RemoveOptionGroupFromProduct.Variables
  1209. >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
  1210. optionGroupId: 'T_3',
  1211. productId: 'T_2',
  1212. });
  1213. removeOptionGuard.assertErrorResult(removeOptionGroupFromProduct);
  1214. expect(removeOptionGroupFromProduct.message).toBe(
  1215. `Cannot remove ProductOptionGroup "curvy-monitor-monitor-size" as it is used by 2 ProductVariants`,
  1216. );
  1217. expect(removeOptionGroupFromProduct.errorCode).toBe(ErrorCode.PRODUCT_OPTION_IN_USE_ERROR);
  1218. expect(removeOptionGroupFromProduct.optionGroupCode).toBe('curvy-monitor-monitor-size');
  1219. expect(removeOptionGroupFromProduct.productVariantCount).toBe(2);
  1220. });
  1221. it('removeOptionGroupFromProduct succeeds if all related ProductVariants are also deleted', async () => {
  1222. const { product } = await adminClient.query<
  1223. GetProductWithVariantsQuery,
  1224. GetProductWithVariantsQueryVariables
  1225. >(GET_PRODUCT_WITH_VARIANTS, { id: 'T_2' });
  1226. // Delete all variants for that product
  1227. for (const variant of product!.variants) {
  1228. await adminClient.query<DeleteProductVariant.Mutation, DeleteProductVariant.Variables>(
  1229. DELETE_PRODUCT_VARIANT,
  1230. {
  1231. id: variant.id,
  1232. },
  1233. );
  1234. }
  1235. const { removeOptionGroupFromProduct } = await adminClient.query<
  1236. RemoveOptionGroupFromProduct.Mutation,
  1237. RemoveOptionGroupFromProduct.Variables
  1238. >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
  1239. optionGroupId: product!.optionGroups[0].id,
  1240. productId: product!.id,
  1241. });
  1242. removeOptionGuard.assertSuccess(removeOptionGroupFromProduct);
  1243. });
  1244. it(
  1245. 'removeOptionGroupFromProduct errors with an invalid productId',
  1246. assertThrowsWithMessage(
  1247. () =>
  1248. adminClient.query<
  1249. RemoveOptionGroupFromProduct.Mutation,
  1250. RemoveOptionGroupFromProduct.Variables
  1251. >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
  1252. optionGroupId: '1',
  1253. productId: '999',
  1254. }),
  1255. `No Product with the id '999' could be found`,
  1256. ),
  1257. );
  1258. it(
  1259. 'removeOptionGroupFromProduct errors with an invalid optionGroupId',
  1260. assertThrowsWithMessage(
  1261. () =>
  1262. adminClient.query<
  1263. RemoveOptionGroupFromProduct.Mutation,
  1264. RemoveOptionGroupFromProduct.Variables
  1265. >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
  1266. optionGroupId: '999',
  1267. productId: newProduct.id,
  1268. }),
  1269. `No ProductOptionGroup with the id '999' could be found`,
  1270. ),
  1271. );
  1272. describe('variants', () => {
  1273. let variants: CreateProductVariants.CreateProductVariants[];
  1274. let optionGroup2: GetOptionGroup.ProductOptionGroup;
  1275. let optionGroup3: GetOptionGroup.ProductOptionGroup;
  1276. beforeAll(async () => {
  1277. optionGroup2 = await createOptionGroup('group-2', ['group2-option-1', 'group2-option-2']);
  1278. optionGroup3 = await createOptionGroup('group-3', ['group3-option-1', 'group3-option-2']);
  1279. await adminClient.query<AddOptionGroupToProduct.Mutation, AddOptionGroupToProduct.Variables>(
  1280. ADD_OPTION_GROUP_TO_PRODUCT,
  1281. {
  1282. optionGroupId: optionGroup2.id,
  1283. productId: newProduct.id,
  1284. },
  1285. );
  1286. await adminClient.query<AddOptionGroupToProduct.Mutation, AddOptionGroupToProduct.Variables>(
  1287. ADD_OPTION_GROUP_TO_PRODUCT,
  1288. {
  1289. optionGroupId: optionGroup3.id,
  1290. productId: newProduct.id,
  1291. },
  1292. );
  1293. });
  1294. it(
  1295. 'createProductVariants throws if optionIds not compatible with product',
  1296. assertThrowsWithMessage(async () => {
  1297. await adminClient.query<CreateProductVariants.Mutation, CreateProductVariants.Variables>(
  1298. CREATE_PRODUCT_VARIANTS,
  1299. {
  1300. input: [
  1301. {
  1302. productId: newProduct.id,
  1303. sku: 'PV1',
  1304. optionIds: [],
  1305. translations: [{ languageCode: LanguageCode.en, name: 'Variant 1' }],
  1306. },
  1307. ],
  1308. },
  1309. );
  1310. }, 'ProductVariant optionIds must include one optionId from each of the groups: group-2, group-3'),
  1311. );
  1312. it(
  1313. 'createProductVariants throws if optionIds are duplicated',
  1314. assertThrowsWithMessage(async () => {
  1315. await adminClient.query<CreateProductVariants.Mutation, CreateProductVariants.Variables>(
  1316. CREATE_PRODUCT_VARIANTS,
  1317. {
  1318. input: [
  1319. {
  1320. productId: newProduct.id,
  1321. sku: 'PV1',
  1322. optionIds: [optionGroup2.options[0].id, optionGroup2.options[1].id],
  1323. translations: [{ languageCode: LanguageCode.en, name: 'Variant 1' }],
  1324. },
  1325. ],
  1326. },
  1327. );
  1328. }, 'ProductVariant optionIds must include one optionId from each of the groups: group-2, group-3'),
  1329. );
  1330. it('createProductVariants works', async () => {
  1331. const { createProductVariants } = await adminClient.query<
  1332. CreateProductVariants.Mutation,
  1333. CreateProductVariants.Variables
  1334. >(CREATE_PRODUCT_VARIANTS, {
  1335. input: [
  1336. {
  1337. productId: newProduct.id,
  1338. sku: 'PV1',
  1339. optionIds: [optionGroup2.options[0].id, optionGroup3.options[0].id],
  1340. translations: [{ languageCode: LanguageCode.en, name: 'Variant 1' }],
  1341. },
  1342. ],
  1343. });
  1344. expect(createProductVariants[0]!.name).toBe('Variant 1');
  1345. expect(createProductVariants[0]!.options.map(pick(['id']))).toContainEqual({
  1346. id: optionGroup2.options[0].id,
  1347. });
  1348. expect(createProductVariants[0]!.options.map(pick(['id']))).toContainEqual({
  1349. id: optionGroup3.options[0].id,
  1350. });
  1351. });
  1352. it('createProductVariants adds multiple variants at once', async () => {
  1353. const { createProductVariants } = await adminClient.query<
  1354. CreateProductVariants.Mutation,
  1355. CreateProductVariants.Variables
  1356. >(CREATE_PRODUCT_VARIANTS, {
  1357. input: [
  1358. {
  1359. productId: newProduct.id,
  1360. sku: 'PV2',
  1361. optionIds: [optionGroup2.options[1].id, optionGroup3.options[0].id],
  1362. translations: [{ languageCode: LanguageCode.en, name: 'Variant 2' }],
  1363. },
  1364. {
  1365. productId: newProduct.id,
  1366. sku: 'PV3',
  1367. optionIds: [optionGroup2.options[1].id, optionGroup3.options[1].id],
  1368. translations: [{ languageCode: LanguageCode.en, name: 'Variant 3' }],
  1369. },
  1370. ],
  1371. });
  1372. const variant2 = createProductVariants.find(v => v!.name === 'Variant 2')!;
  1373. const variant3 = createProductVariants.find(v => v!.name === 'Variant 3')!;
  1374. expect(variant2.options.map(pick(['id']))).toContainEqual({ id: optionGroup2.options[1].id });
  1375. expect(variant2.options.map(pick(['id']))).toContainEqual({ id: optionGroup3.options[0].id });
  1376. expect(variant3.options.map(pick(['id']))).toContainEqual({ id: optionGroup2.options[1].id });
  1377. expect(variant3.options.map(pick(['id']))).toContainEqual({ id: optionGroup3.options[1].id });
  1378. variants = createProductVariants.filter(notNullOrUndefined);
  1379. });
  1380. it(
  1381. 'createProductVariants throws if options combination already exists',
  1382. assertThrowsWithMessage(async () => {
  1383. await adminClient.query<CreateProductVariants.Mutation, CreateProductVariants.Variables>(
  1384. CREATE_PRODUCT_VARIANTS,
  1385. {
  1386. input: [
  1387. {
  1388. productId: newProduct.id,
  1389. sku: 'PV2',
  1390. optionIds: [optionGroup2.options[0].id, optionGroup3.options[0].id],
  1391. translations: [{ languageCode: LanguageCode.en, name: 'Variant 2' }],
  1392. },
  1393. ],
  1394. },
  1395. );
  1396. }, 'A ProductVariant with the selected options already exists: Variant 1'),
  1397. );
  1398. it('updateProductVariants updates variants', async () => {
  1399. const firstVariant = variants[0];
  1400. const { updateProductVariants } = await adminClient.query<
  1401. UpdateProductVariants.Mutation,
  1402. UpdateProductVariants.Variables
  1403. >(UPDATE_PRODUCT_VARIANTS, {
  1404. input: [
  1405. {
  1406. id: firstVariant.id,
  1407. translations: firstVariant.translations,
  1408. sku: 'ABC',
  1409. price: 432,
  1410. },
  1411. ],
  1412. });
  1413. const updatedVariant = updateProductVariants[0];
  1414. if (!updatedVariant) {
  1415. fail('no updated variant returned.');
  1416. return;
  1417. }
  1418. expect(updatedVariant.sku).toBe('ABC');
  1419. expect(updatedVariant.price).toBe(432);
  1420. });
  1421. // https://github.com/vendure-ecommerce/vendure/issues/1101
  1422. it('after update, the updatedAt should be modified', async () => {
  1423. // Pause for a second to ensure the updatedAt date is more than 1s
  1424. // later than the createdAt date, since sqlite does not seem to store
  1425. // down to millisecond resolution.
  1426. await new Promise(resolve => setTimeout(resolve, 1000));
  1427. const firstVariant = variants[0];
  1428. const { updateProductVariants } = await adminClient.query<
  1429. UpdateProductVariants.Mutation,
  1430. UpdateProductVariants.Variables
  1431. >(UPDATE_PRODUCT_VARIANTS, {
  1432. input: [
  1433. {
  1434. id: firstVariant.id,
  1435. translations: firstVariant.translations,
  1436. sku: 'ABCD',
  1437. price: 432,
  1438. },
  1439. ],
  1440. });
  1441. const updatedVariant = updateProductVariants.find(v => v?.id === variants[0].id);
  1442. expect(updatedVariant?.updatedAt).not.toBe(updatedVariant?.createdAt);
  1443. });
  1444. it('updateProductVariants updates assets', async () => {
  1445. const firstVariant = variants[0];
  1446. const result = await adminClient.query<
  1447. UpdateProductVariants.Mutation,
  1448. UpdateProductVariants.Variables
  1449. >(UPDATE_PRODUCT_VARIANTS, {
  1450. input: [
  1451. {
  1452. id: firstVariant.id,
  1453. assetIds: ['T_1', 'T_2'],
  1454. featuredAssetId: 'T_2',
  1455. },
  1456. ],
  1457. });
  1458. const updatedVariant = result.updateProductVariants[0];
  1459. if (!updatedVariant) {
  1460. fail('no updated variant returned.');
  1461. return;
  1462. }
  1463. expect(updatedVariant.assets.map(a => a.id)).toEqual(['T_1', 'T_2']);
  1464. expect(updatedVariant.featuredAsset!.id).toBe('T_2');
  1465. });
  1466. it('updateProductVariants updates assets again', async () => {
  1467. const firstVariant = variants[0];
  1468. const result = await adminClient.query<
  1469. UpdateProductVariants.Mutation,
  1470. UpdateProductVariants.Variables
  1471. >(UPDATE_PRODUCT_VARIANTS, {
  1472. input: [
  1473. {
  1474. id: firstVariant.id,
  1475. assetIds: ['T_4', 'T_3'],
  1476. featuredAssetId: 'T_4',
  1477. },
  1478. ],
  1479. });
  1480. const updatedVariant = result.updateProductVariants[0];
  1481. if (!updatedVariant) {
  1482. fail('no updated variant returned.');
  1483. return;
  1484. }
  1485. expect(updatedVariant.assets.map(a => a.id)).toEqual(['T_4', 'T_3']);
  1486. expect(updatedVariant.featuredAsset!.id).toBe('T_4');
  1487. });
  1488. it('updateProductVariants updates taxCategory and price', async () => {
  1489. const firstVariant = variants[0];
  1490. const result = await adminClient.query<
  1491. UpdateProductVariants.Mutation,
  1492. UpdateProductVariants.Variables
  1493. >(UPDATE_PRODUCT_VARIANTS, {
  1494. input: [
  1495. {
  1496. id: firstVariant.id,
  1497. price: 105,
  1498. taxCategoryId: 'T_2',
  1499. },
  1500. ],
  1501. });
  1502. const updatedVariant = result.updateProductVariants[0];
  1503. if (!updatedVariant) {
  1504. fail('no updated variant returned.');
  1505. return;
  1506. }
  1507. expect(updatedVariant.price).toBe(105);
  1508. expect(updatedVariant.taxCategory.id).toBe('T_2');
  1509. });
  1510. it('updateProductVariants updates facetValues', async () => {
  1511. const firstVariant = variants[0];
  1512. const result = await adminClient.query<
  1513. UpdateProductVariants.Mutation,
  1514. UpdateProductVariants.Variables
  1515. >(UPDATE_PRODUCT_VARIANTS, {
  1516. input: [
  1517. {
  1518. id: firstVariant.id,
  1519. facetValueIds: ['T_1'],
  1520. },
  1521. ],
  1522. });
  1523. const updatedVariant = result.updateProductVariants[0];
  1524. if (!updatedVariant) {
  1525. fail('no updated variant returned.');
  1526. return;
  1527. }
  1528. expect(updatedVariant.facetValues.length).toBe(1);
  1529. expect(updatedVariant.facetValues[0].id).toBe('T_1');
  1530. });
  1531. it(
  1532. 'updateProductVariants throws with an invalid variant id',
  1533. assertThrowsWithMessage(
  1534. () =>
  1535. adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
  1536. UPDATE_PRODUCT_VARIANTS,
  1537. {
  1538. input: [
  1539. {
  1540. id: 'T_999',
  1541. translations: variants[0].translations,
  1542. sku: 'ABC',
  1543. price: 432,
  1544. },
  1545. ],
  1546. },
  1547. ),
  1548. `No ProductVariant with the id '999' could be found`,
  1549. ),
  1550. );
  1551. let deletedVariant: ProductVariantFragment;
  1552. it('deleteProductVariant', async () => {
  1553. const result1 = await adminClient.query<
  1554. GetProductWithVariants.Query,
  1555. GetProductWithVariants.Variables
  1556. >(GET_PRODUCT_WITH_VARIANTS, {
  1557. id: newProduct.id,
  1558. });
  1559. const sortedVariantIds = result1.product!.variants.map(v => v.id).sort();
  1560. expect(sortedVariantIds).toEqual(['T_35', 'T_36', 'T_37']);
  1561. const { deleteProductVariant } = await adminClient.query<
  1562. DeleteProductVariant.Mutation,
  1563. DeleteProductVariant.Variables
  1564. >(DELETE_PRODUCT_VARIANT, {
  1565. id: sortedVariantIds[0],
  1566. });
  1567. expect(deleteProductVariant.result).toBe(DeletionResult.DELETED);
  1568. const result2 = await adminClient.query<
  1569. GetProductWithVariants.Query,
  1570. GetProductWithVariants.Variables
  1571. >(GET_PRODUCT_WITH_VARIANTS, {
  1572. id: newProduct.id,
  1573. });
  1574. expect(result2.product!.variants.map(v => v.id).sort()).toEqual(['T_36', 'T_37']);
  1575. deletedVariant = result1.product?.variants.find(v => v.id === 'T_35')!;
  1576. });
  1577. /** Testing https://github.com/vendure-ecommerce/vendure/issues/412 **/
  1578. it('createProductVariants ignores deleted variants when checking for existing combinations', async () => {
  1579. const { createProductVariants } = await adminClient.query<
  1580. CreateProductVariants.Mutation,
  1581. CreateProductVariants.Variables
  1582. >(CREATE_PRODUCT_VARIANTS, {
  1583. input: [
  1584. {
  1585. productId: newProduct.id,
  1586. sku: 'RE1',
  1587. optionIds: [deletedVariant.options[0].id, deletedVariant.options[1].id],
  1588. translations: [{ languageCode: LanguageCode.en, name: 'Re-created Variant' }],
  1589. },
  1590. ],
  1591. });
  1592. expect(createProductVariants.length).toBe(1);
  1593. expect(createProductVariants[0]!.options.map(o => o.code).sort()).toEqual(
  1594. deletedVariant.options.map(o => o.code).sort(),
  1595. );
  1596. });
  1597. // https://github.com/vendure-ecommerce/vendure/issues/980
  1598. it('creating variants in a non-default language', async () => {
  1599. const { createProduct } = await adminClient.query<
  1600. CreateProduct.Mutation,
  1601. CreateProduct.Variables
  1602. >(CREATE_PRODUCT, {
  1603. input: {
  1604. translations: [
  1605. {
  1606. languageCode: LanguageCode.de,
  1607. name: 'Ananas',
  1608. slug: 'ananas',
  1609. description: 'Yummy Ananas',
  1610. },
  1611. ],
  1612. },
  1613. });
  1614. const { createProductVariants } = await adminClient.query<
  1615. CreateProductVariants.Mutation,
  1616. CreateProductVariants.Variables
  1617. >(CREATE_PRODUCT_VARIANTS, {
  1618. input: [
  1619. {
  1620. productId: createProduct.id,
  1621. sku: 'AN1110111',
  1622. optionIds: [],
  1623. translations: [{ languageCode: LanguageCode.de, name: 'Ananas Klein' }],
  1624. },
  1625. ],
  1626. });
  1627. expect(createProductVariants.length).toBe(1);
  1628. expect(createProductVariants[0]?.name).toBe('Ananas Klein');
  1629. const { product } = await adminClient.query<
  1630. GetProductWithVariants.Query,
  1631. GetProductWithVariants.Variables
  1632. >(
  1633. GET_PRODUCT_WITH_VARIANTS,
  1634. {
  1635. id: createProduct.id,
  1636. },
  1637. { languageCode: LanguageCode.en },
  1638. );
  1639. expect(product?.variants.length).toBe(1);
  1640. });
  1641. // https://github.com/vendure-ecommerce/vendure/issues/1631
  1642. describe('changing the Channel default language', () => {
  1643. let productId: string;
  1644. function getProductWithVariantsInLanguage(
  1645. id: string,
  1646. languageCode: LanguageCode,
  1647. variantListOptions?: ProductVariantListOptions,
  1648. ) {
  1649. return adminClient.query<
  1650. GetProductWithVariantList.Query,
  1651. GetProductWithVariantList.Variables
  1652. >(GET_PRODUCT_WITH_VARIANT_LIST, { id, variantListOptions }, { languageCode });
  1653. }
  1654. beforeAll(async () => {
  1655. await adminClient.query<UpdateGlobalSettings.Mutation, UpdateGlobalSettings.Variables>(
  1656. UPDATE_GLOBAL_SETTINGS,
  1657. {
  1658. input: {
  1659. availableLanguages: [LanguageCode.en, LanguageCode.de],
  1660. },
  1661. },
  1662. );
  1663. const { createProduct } = await adminClient.query<
  1664. CreateProduct.Mutation,
  1665. CreateProduct.Variables
  1666. >(CREATE_PRODUCT, {
  1667. input: {
  1668. translations: [
  1669. {
  1670. languageCode: LanguageCode.en,
  1671. name: 'Bottle',
  1672. slug: 'bottle',
  1673. description: 'A container for liquids',
  1674. },
  1675. ],
  1676. },
  1677. });
  1678. productId = createProduct.id;
  1679. await adminClient.query<CreateProductVariants.Mutation, CreateProductVariants.Variables>(
  1680. CREATE_PRODUCT_VARIANTS,
  1681. {
  1682. input: [
  1683. {
  1684. productId,
  1685. sku: 'BOTTLE111',
  1686. optionIds: [],
  1687. translations: [{ languageCode: LanguageCode.en, name: 'Bottle' }],
  1688. },
  1689. ],
  1690. },
  1691. );
  1692. });
  1693. afterAll(async () => {
  1694. // Restore the default language to English for the subsequent tests
  1695. await adminClient.query<UpdateChannel.Mutation, UpdateChannel.Variables>(UPDATE_CHANNEL, {
  1696. input: {
  1697. id: 'T_1',
  1698. defaultLanguageCode: LanguageCode.en,
  1699. },
  1700. });
  1701. });
  1702. it('returns all variants', async () => {
  1703. const { product: product1 } = await adminClient.query<
  1704. GetProductWithVariants.Query,
  1705. GetProductWithVariants.Variables
  1706. >(
  1707. GET_PRODUCT_WITH_VARIANTS,
  1708. {
  1709. id: productId,
  1710. },
  1711. { languageCode: LanguageCode.en },
  1712. );
  1713. expect(product1?.variants.length).toBe(1);
  1714. // Change the default language of the channel to "de"
  1715. const { updateChannel } = await adminClient.query<
  1716. UpdateChannel.Mutation,
  1717. UpdateChannel.Variables
  1718. >(UPDATE_CHANNEL, {
  1719. input: {
  1720. id: 'T_1',
  1721. defaultLanguageCode: LanguageCode.de,
  1722. },
  1723. });
  1724. updateChannelGuard.assertSuccess(updateChannel);
  1725. expect(updateChannel.defaultLanguageCode).toBe(LanguageCode.de);
  1726. // Fetch the product in en, it should still return 1 variant
  1727. const { product: product2 } = await getProductWithVariantsInLanguage(
  1728. productId,
  1729. LanguageCode.en,
  1730. );
  1731. expect(product2?.variantList.items.length).toBe(1);
  1732. // Fetch the product in de, it should still return 1 variant
  1733. const { product: product3 } = await getProductWithVariantsInLanguage(
  1734. productId,
  1735. LanguageCode.de,
  1736. );
  1737. expect(product3?.variantList.items.length).toBe(1);
  1738. });
  1739. it('returns all variants when sorting on variant name', async () => {
  1740. // Fetch the product in en, it should still return 1 variant
  1741. const { product: product1 } = await getProductWithVariantsInLanguage(
  1742. productId,
  1743. LanguageCode.en,
  1744. { sort: { name: SortOrder.ASC } },
  1745. );
  1746. expect(product1?.variantList.items.length).toBe(1);
  1747. // Fetch the product in de, it should still return 1 variant
  1748. const { product: product2 } = await getProductWithVariantsInLanguage(
  1749. productId,
  1750. LanguageCode.de,
  1751. { sort: { name: SortOrder.ASC } },
  1752. );
  1753. expect(product2?.variantList.items.length).toBe(1);
  1754. });
  1755. });
  1756. });
  1757. });
  1758. describe('deletion', () => {
  1759. let allProducts: GetProductList.Items[];
  1760. let productToDelete: GetProductWithVariants.Product;
  1761. beforeAll(async () => {
  1762. const result = await adminClient.query<GetProductList.Query, GetProductList.Variables>(
  1763. GET_PRODUCT_LIST,
  1764. {
  1765. options: {
  1766. sort: {
  1767. id: SortOrder.ASC,
  1768. },
  1769. },
  1770. },
  1771. );
  1772. allProducts = result.products.items;
  1773. });
  1774. it('deletes a product', async () => {
  1775. const { product } = await adminClient.query<
  1776. GetProductWithVariants.Query,
  1777. GetProductWithVariants.Variables
  1778. >(GET_PRODUCT_WITH_VARIANTS, {
  1779. id: allProducts[0].id,
  1780. });
  1781. const result = await adminClient.query<DeleteProduct.Mutation, DeleteProduct.Variables>(
  1782. DELETE_PRODUCT,
  1783. { id: product!.id },
  1784. );
  1785. expect(result.deleteProduct).toEqual({ result: DeletionResult.DELETED });
  1786. productToDelete = product!;
  1787. });
  1788. it('cannot get a deleted product', async () => {
  1789. const { product } = await adminClient.query<
  1790. GetProductWithVariants.Query,
  1791. GetProductWithVariants.Variables
  1792. >(GET_PRODUCT_WITH_VARIANTS, {
  1793. id: productToDelete.id,
  1794. });
  1795. expect(product).toBe(null);
  1796. });
  1797. // https://github.com/vendure-ecommerce/vendure/issues/1096
  1798. it('variants of deleted product are also deleted', async () => {
  1799. for (const variant of productToDelete.variants) {
  1800. const { productVariant } = await adminClient.query<
  1801. GetProductVariant.Query,
  1802. GetProductVariant.Variables
  1803. >(GET_PRODUCT_VARIANT, {
  1804. id: variant.id,
  1805. });
  1806. expect(productVariant).toBe(null);
  1807. }
  1808. });
  1809. it('deleted product omitted from list', async () => {
  1810. const result = await adminClient.query<GetProductList.Query>(GET_PRODUCT_LIST);
  1811. expect(result.products.items.length).toBe(allProducts.length - 1);
  1812. expect(result.products.items.map(c => c.id).includes(productToDelete.id)).toBe(false);
  1813. });
  1814. it(
  1815. 'updateProduct throws for deleted product',
  1816. assertThrowsWithMessage(
  1817. () =>
  1818. adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
  1819. input: {
  1820. id: productToDelete.id,
  1821. facetValueIds: ['T_1'],
  1822. },
  1823. }),
  1824. `No Product with the id '1' could be found`,
  1825. ),
  1826. );
  1827. it(
  1828. 'addOptionGroupToProduct throws for deleted product',
  1829. assertThrowsWithMessage(
  1830. () =>
  1831. adminClient.query<AddOptionGroupToProduct.Mutation, AddOptionGroupToProduct.Variables>(
  1832. ADD_OPTION_GROUP_TO_PRODUCT,
  1833. {
  1834. optionGroupId: 'T_1',
  1835. productId: productToDelete.id,
  1836. },
  1837. ),
  1838. `No Product with the id '1' could be found`,
  1839. ),
  1840. );
  1841. it(
  1842. 'removeOptionGroupToProduct throws for deleted product',
  1843. assertThrowsWithMessage(
  1844. () =>
  1845. adminClient.query<
  1846. RemoveOptionGroupFromProduct.Mutation,
  1847. RemoveOptionGroupFromProduct.Variables
  1848. >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
  1849. optionGroupId: 'T_1',
  1850. productId: productToDelete.id,
  1851. }),
  1852. `No Product with the id '1' could be found`,
  1853. ),
  1854. );
  1855. // https://github.com/vendure-ecommerce/vendure/issues/558
  1856. it('slug of a deleted product can be re-used', async () => {
  1857. const result = await adminClient.query<CreateProduct.Mutation, CreateProduct.Variables>(
  1858. CREATE_PRODUCT,
  1859. {
  1860. input: {
  1861. translations: [
  1862. {
  1863. languageCode: LanguageCode.en,
  1864. name: 'Product reusing deleted slug',
  1865. slug: productToDelete.slug,
  1866. description: 'stuff',
  1867. },
  1868. ],
  1869. },
  1870. },
  1871. );
  1872. expect(result.createProduct.slug).toBe(productToDelete.slug);
  1873. });
  1874. // https://github.com/vendure-ecommerce/vendure/issues/1505
  1875. it('attempting to re-use deleted slug twice is not allowed', async () => {
  1876. const result = await adminClient.query<CreateProduct.Mutation, CreateProduct.Variables>(
  1877. CREATE_PRODUCT,
  1878. {
  1879. input: {
  1880. translations: [
  1881. {
  1882. languageCode: LanguageCode.en,
  1883. name: 'Product reusing deleted slug',
  1884. slug: productToDelete.slug,
  1885. description: 'stuff',
  1886. },
  1887. ],
  1888. },
  1889. },
  1890. );
  1891. expect(result.createProduct.slug).not.toBe(productToDelete.slug);
  1892. expect(result.createProduct.slug).toBe('laptop-2');
  1893. });
  1894. // https://github.com/vendure-ecommerce/vendure/issues/800
  1895. it('product can be fetched by slug of a deleted product', async () => {
  1896. const { product } = await adminClient.query<GetProductSimple.Query, GetProductSimple.Variables>(
  1897. GET_PRODUCT_SIMPLE,
  1898. { slug: productToDelete.slug },
  1899. );
  1900. if (!product) {
  1901. fail('Product not found');
  1902. return;
  1903. }
  1904. expect(product.slug).toBe(productToDelete.slug);
  1905. });
  1906. });
  1907. async function createOptionGroup(name: string, options: string[]) {
  1908. const { createProductOptionGroup } = await adminClient.query<
  1909. CreateProductOptionGroupMutation,
  1910. CreateProductOptionGroupMutationVariables
  1911. >(CREATE_PRODUCT_OPTION_GROUP, {
  1912. input: {
  1913. code: name.toLowerCase(),
  1914. translations: [{ languageCode: LanguageCode.en, name }],
  1915. options: options.map(option => ({
  1916. code: option.toLowerCase(),
  1917. translations: [{ languageCode: LanguageCode.en, name: option }],
  1918. })),
  1919. },
  1920. });
  1921. return createProductOptionGroup;
  1922. }
  1923. });
  1924. export const REMOVE_OPTION_GROUP_FROM_PRODUCT = gql`
  1925. mutation RemoveOptionGroupFromProduct($productId: ID!, $optionGroupId: ID!) {
  1926. removeOptionGroupFromProduct(productId: $productId, optionGroupId: $optionGroupId) {
  1927. ...ProductWithOptions
  1928. ... on ProductOptionInUseError {
  1929. errorCode
  1930. message
  1931. optionGroupCode
  1932. productVariantCount
  1933. }
  1934. }
  1935. }
  1936. ${PRODUCT_WITH_OPTIONS_FRAGMENT}
  1937. `;
  1938. export const GET_OPTION_GROUP = gql`
  1939. query GetOptionGroup($id: ID!) {
  1940. productOptionGroup(id: $id) {
  1941. id
  1942. code
  1943. options {
  1944. id
  1945. code
  1946. }
  1947. }
  1948. }
  1949. `;
  1950. export const GET_PRODUCT_VARIANT = gql`
  1951. query GetProductVariant($id: ID!) {
  1952. productVariant(id: $id) {
  1953. id
  1954. name
  1955. }
  1956. }
  1957. `;
  1958. export const GET_PRODUCT_WITH_VARIANT_LIST = gql`
  1959. query GetProductWithVariantList($id: ID, $variantListOptions: ProductVariantListOptions) {
  1960. product(id: $id) {
  1961. id
  1962. variantList(options: $variantListOptions) {
  1963. items {
  1964. ...ProductVariant
  1965. }
  1966. totalItems
  1967. }
  1968. }
  1969. }
  1970. ${PRODUCT_VARIANT_FRAGMENT}
  1971. `;