product.e2e-spec.ts 84 KB

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