product.e2e-spec.ts 92 KB

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