product.e2e-spec.ts 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689
  1. import {
  2. AddOptionGroupToProduct,
  3. CreateProduct,
  4. DeletionResult,
  5. GenerateProductVariants,
  6. GetAssetList,
  7. GetProductList,
  8. GetProductWithVariants,
  9. LanguageCode,
  10. ProductWithVariants,
  11. RemoveOptionGroupFromProduct,
  12. SortOrder,
  13. UpdateProduct,
  14. UpdateProductVariants,
  15. } from '@vendure/common/lib/generated-types';
  16. import { omit } from '@vendure/common/lib/omit';
  17. import gql from 'graphql-tag';
  18. import path from 'path';
  19. import {
  20. ADD_OPTION_GROUP_TO_PRODUCT,
  21. CREATE_PRODUCT,
  22. GENERATE_PRODUCT_VARIANTS,
  23. GET_ASSET_LIST,
  24. GET_PRODUCT_LIST,
  25. GET_PRODUCT_WITH_VARIANTS,
  26. REMOVE_OPTION_GROUP_FROM_PRODUCT,
  27. UPDATE_PRODUCT,
  28. UPDATE_PRODUCT_VARIANTS,
  29. } from '../../../admin-ui/src/app/data/definitions/product-definitions';
  30. import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
  31. import { TestAdminClient } from './test-client';
  32. import { TestServer } from './test-server';
  33. import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
  34. // tslint:disable:no-non-null-assertion
  35. describe('Product resolver', () => {
  36. const client = new TestAdminClient();
  37. const server = new TestServer();
  38. beforeAll(async () => {
  39. const token = await server.init({
  40. customerCount: 1,
  41. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
  42. });
  43. await client.init();
  44. }, TEST_SETUP_TIMEOUT_MS);
  45. afterAll(async () => {
  46. await server.destroy();
  47. });
  48. describe('products list query', () => {
  49. it('returns all products when no options passed', async () => {
  50. const result = await client.query<GetProductList.Query, GetProductList.Variables>(
  51. GET_PRODUCT_LIST,
  52. {
  53. languageCode: LanguageCode.en,
  54. },
  55. );
  56. expect(result.products.items.length).toBe(20);
  57. expect(result.products.totalItems).toBe(20);
  58. });
  59. it('limits result set with skip & take', async () => {
  60. const result = await client.query<GetProductList.Query, GetProductList.Variables>(
  61. GET_PRODUCT_LIST,
  62. {
  63. languageCode: LanguageCode.en,
  64. options: {
  65. skip: 0,
  66. take: 3,
  67. },
  68. },
  69. );
  70. expect(result.products.items.length).toBe(3);
  71. expect(result.products.totalItems).toBe(20);
  72. });
  73. it('filters by name', async () => {
  74. const result = await client.query<GetProductList.Query, GetProductList.Variables>(
  75. GET_PRODUCT_LIST,
  76. {
  77. languageCode: LanguageCode.en,
  78. options: {
  79. filter: {
  80. name: {
  81. contains: 'skateboard',
  82. },
  83. },
  84. },
  85. },
  86. );
  87. expect(result.products.items.length).toBe(1);
  88. expect(result.products.items[0].name).toBe('Cruiser Skateboard');
  89. });
  90. it('sorts by name', async () => {
  91. const result = await client.query<GetProductList.Query, GetProductList.Variables>(
  92. GET_PRODUCT_LIST,
  93. {
  94. languageCode: LanguageCode.en,
  95. options: {
  96. sort: {
  97. name: SortOrder.ASC,
  98. },
  99. },
  100. },
  101. );
  102. expect(result.products.items.map(p => p.name)).toMatchSnapshot();
  103. });
  104. });
  105. describe('product query', () => {
  106. it('returns expected properties', async () => {
  107. const result = await client.query<GetProductWithVariants.Query, GetProductWithVariants.Variables>(
  108. GET_PRODUCT_WITH_VARIANTS,
  109. {
  110. languageCode: LanguageCode.en,
  111. id: 'T_2',
  112. },
  113. );
  114. if (!result.product) {
  115. fail('Product not found');
  116. return;
  117. }
  118. expect(omit(result.product, ['variants'])).toMatchSnapshot();
  119. expect(result.product.variants.length).toBe(2);
  120. });
  121. it('ProductVariant price properties are correct', async () => {
  122. const result = await client.query<GetProductWithVariants.Query, GetProductWithVariants.Variables>(
  123. GET_PRODUCT_WITH_VARIANTS,
  124. {
  125. languageCode: LanguageCode.en,
  126. id: 'T_2',
  127. },
  128. );
  129. if (!result.product) {
  130. fail('Product not found');
  131. return;
  132. }
  133. expect(result.product.variants[0].price).toBe(14374);
  134. expect(result.product.variants[0].taxCategory).toEqual({
  135. id: 'T_1',
  136. name: 'Standard Tax',
  137. });
  138. });
  139. it('returns null when id not found', async () => {
  140. const result = await client.query<GetProductWithVariants.Query, GetProductWithVariants.Variables>(
  141. GET_PRODUCT_WITH_VARIANTS,
  142. {
  143. languageCode: LanguageCode.en,
  144. id: 'bad_id',
  145. },
  146. );
  147. expect(result.product).toBeNull();
  148. });
  149. });
  150. describe('product mutation', () => {
  151. let newProduct: ProductWithVariants.Fragment;
  152. it('createProduct creates a new Product', async () => {
  153. const result = await client.query<CreateProduct.Mutation, CreateProduct.Variables>(
  154. CREATE_PRODUCT,
  155. {
  156. input: {
  157. translations: [
  158. {
  159. languageCode: LanguageCode.en,
  160. name: 'en Baked Potato',
  161. slug: 'en-baked-potato',
  162. description: 'A baked potato',
  163. },
  164. {
  165. languageCode: LanguageCode.de,
  166. name: 'de Baked Potato',
  167. slug: 'de-baked-potato',
  168. description: 'Eine baked Erdapfel',
  169. },
  170. ],
  171. },
  172. },
  173. );
  174. newProduct = result.createProduct;
  175. expect(newProduct).toMatchSnapshot();
  176. });
  177. it('createProduct creates a new Product with assets', async () => {
  178. const assetsResult = await client.query<GetAssetList.Query, GetAssetList.Variables>(
  179. GET_ASSET_LIST,
  180. );
  181. const assetIds = assetsResult.assets.items.slice(0, 2).map(a => a.id);
  182. const featuredAssetId = assetsResult.assets.items[0].id;
  183. const result = await client.query<CreateProduct.Mutation, CreateProduct.Variables>(
  184. CREATE_PRODUCT,
  185. {
  186. input: {
  187. assetIds,
  188. featuredAssetId,
  189. translations: [
  190. {
  191. languageCode: LanguageCode.en,
  192. name: 'en Has Assets',
  193. slug: 'en-has-assets',
  194. description: 'A product with assets',
  195. },
  196. ],
  197. },
  198. },
  199. );
  200. expect(result.createProduct.assets.map(a => a.id)).toEqual(assetIds);
  201. expect(result.createProduct.featuredAsset!.id).toBe(featuredAssetId);
  202. });
  203. it('updateProduct updates a Product', async () => {
  204. const result = await client.query<UpdateProduct.Mutation, UpdateProduct.Variables>(
  205. UPDATE_PRODUCT,
  206. {
  207. input: {
  208. id: newProduct.id,
  209. translations: [
  210. {
  211. languageCode: LanguageCode.en,
  212. name: 'en Mashed Potato',
  213. slug: 'en-mashed-potato',
  214. description: 'A blob of mashed potato',
  215. },
  216. {
  217. languageCode: LanguageCode.de,
  218. name: 'de Mashed Potato',
  219. slug: 'de-mashed-potato',
  220. description: 'Eine blob von gemashed Erdapfel',
  221. },
  222. ],
  223. },
  224. },
  225. );
  226. expect(result.updateProduct).toMatchSnapshot();
  227. });
  228. it('updateProduct accepts partial input', async () => {
  229. const result = await client.query<UpdateProduct.Mutation, UpdateProduct.Variables>(
  230. UPDATE_PRODUCT,
  231. {
  232. input: {
  233. id: newProduct.id,
  234. translations: [
  235. {
  236. languageCode: LanguageCode.en,
  237. name: 'en Very Mashed Potato',
  238. },
  239. ],
  240. },
  241. },
  242. );
  243. expect(result.updateProduct.translations.length).toBe(2);
  244. expect(result.updateProduct.translations[0].name).toBe('en Very Mashed Potato');
  245. expect(result.updateProduct.translations[0].description).toBe('A blob of mashed potato');
  246. expect(result.updateProduct.translations[1].name).toBe('de Mashed Potato');
  247. });
  248. it('updateProduct adds Assets to a product and sets featured asset', async () => {
  249. const assetsResult = await client.query<GetAssetList.Query, GetAssetList.Variables>(
  250. GET_ASSET_LIST,
  251. );
  252. const assetIds = assetsResult.assets.items.map(a => a.id);
  253. const featuredAssetId = assetsResult.assets.items[2].id;
  254. const result = await client.query<UpdateProduct.Mutation, UpdateProduct.Variables>(
  255. UPDATE_PRODUCT,
  256. {
  257. input: {
  258. id: newProduct.id,
  259. assetIds,
  260. featuredAssetId,
  261. },
  262. },
  263. );
  264. expect(result.updateProduct.assets.map(a => a.id)).toEqual(assetIds);
  265. expect(result.updateProduct.featuredAsset!.id).toBe(featuredAssetId);
  266. });
  267. it('updateProduct sets a featured asset', async () => {
  268. const productResult = await client.query<
  269. GetProductWithVariants.Query,
  270. GetProductWithVariants.Variables
  271. >(GET_PRODUCT_WITH_VARIANTS, {
  272. id: newProduct.id,
  273. languageCode: LanguageCode.en,
  274. });
  275. const assets = productResult.product!.assets;
  276. const result = await client.query<UpdateProduct.Mutation, UpdateProduct.Variables>(
  277. UPDATE_PRODUCT,
  278. {
  279. input: {
  280. id: newProduct.id,
  281. featuredAssetId: assets[0].id,
  282. },
  283. },
  284. );
  285. expect(result.updateProduct.featuredAsset!.id).toBe(assets[0].id);
  286. });
  287. it('updateProduct updates FacetValues', async () => {
  288. const result = await client.query<UpdateProduct.Mutation, UpdateProduct.Variables>(
  289. UPDATE_PRODUCT,
  290. {
  291. input: {
  292. id: newProduct.id,
  293. facetValueIds: ['T_1'],
  294. },
  295. },
  296. );
  297. expect(result.updateProduct.facetValues.length).toEqual(1);
  298. });
  299. it(
  300. 'updateProduct errors with an invalid productId',
  301. assertThrowsWithMessage(
  302. () =>
  303. client.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
  304. input: {
  305. id: '999',
  306. translations: [
  307. {
  308. languageCode: LanguageCode.en,
  309. name: 'en Mashed Potato',
  310. slug: 'en-mashed-potato',
  311. description: 'A blob of mashed potato',
  312. },
  313. {
  314. languageCode: LanguageCode.de,
  315. name: 'de Mashed Potato',
  316. slug: 'de-mashed-potato',
  317. description: 'Eine blob von gemashed Erdapfel',
  318. },
  319. ],
  320. },
  321. }),
  322. `No Product with the id '999' could be found`,
  323. ),
  324. );
  325. it('addOptionGroupToProduct adds an option group', async () => {
  326. const result = await client.query<
  327. AddOptionGroupToProduct.Mutation,
  328. AddOptionGroupToProduct.Variables
  329. >(ADD_OPTION_GROUP_TO_PRODUCT, {
  330. optionGroupId: 'T_2',
  331. productId: newProduct.id,
  332. });
  333. expect(result.addOptionGroupToProduct.optionGroups.length).toBe(1);
  334. expect(result.addOptionGroupToProduct.optionGroups[0].id).toBe('T_2');
  335. });
  336. it(
  337. 'addOptionGroupToProduct errors with an invalid productId',
  338. assertThrowsWithMessage(
  339. () =>
  340. client.query<AddOptionGroupToProduct.Mutation, AddOptionGroupToProduct.Variables>(
  341. ADD_OPTION_GROUP_TO_PRODUCT,
  342. {
  343. optionGroupId: 'T_1',
  344. productId: '999',
  345. },
  346. ),
  347. `No Product with the id '999' could be found`,
  348. ),
  349. );
  350. it(
  351. 'addOptionGroupToProduct errors with an invalid optionGroupId',
  352. assertThrowsWithMessage(
  353. () =>
  354. client.query<AddOptionGroupToProduct.Mutation, AddOptionGroupToProduct.Variables>(
  355. ADD_OPTION_GROUP_TO_PRODUCT,
  356. {
  357. optionGroupId: '999',
  358. productId: newProduct.id,
  359. },
  360. ),
  361. `No ProductOptionGroup with the id '999' could be found`,
  362. ),
  363. );
  364. it('removeOptionGroupFromProduct removes an option group', async () => {
  365. const result = await client.query<
  366. RemoveOptionGroupFromProduct.Mutation,
  367. RemoveOptionGroupFromProduct.Variables
  368. >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
  369. optionGroupId: 'T_1',
  370. productId: 'T_1',
  371. });
  372. expect(result.removeOptionGroupFromProduct.optionGroups.length).toBe(1);
  373. expect(result.removeOptionGroupFromProduct.optionGroups[0].code).toBe('laptop-ram');
  374. });
  375. it(
  376. 'removeOptionGroupFromProduct errors with an invalid productId',
  377. assertThrowsWithMessage(
  378. () =>
  379. client.query<
  380. RemoveOptionGroupFromProduct.Mutation,
  381. RemoveOptionGroupFromProduct.Variables
  382. >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
  383. optionGroupId: '1',
  384. productId: '999',
  385. }),
  386. `No Product with the id '999' could be found`,
  387. ),
  388. );
  389. describe('variants', () => {
  390. let variants: ProductWithVariants.Variants[];
  391. it(
  392. 'generateVariantsForProduct throws with an invalid productId',
  393. assertThrowsWithMessage(
  394. () =>
  395. client.query<GenerateProductVariants.Mutation, GenerateProductVariants.Variables>(
  396. GENERATE_PRODUCT_VARIANTS,
  397. {
  398. productId: '999',
  399. },
  400. ),
  401. `No Product with the id '999' could be found`,
  402. ),
  403. );
  404. it(
  405. 'generateVariantsForProduct throws with an invalid defaultTaxCategoryId',
  406. assertThrowsWithMessage(
  407. () =>
  408. client.query<GenerateProductVariants.Mutation, GenerateProductVariants.Variables>(
  409. GENERATE_PRODUCT_VARIANTS,
  410. {
  411. productId: newProduct.id,
  412. defaultTaxCategoryId: '999',
  413. },
  414. ),
  415. `No TaxCategory with the id '999' could be found`,
  416. ),
  417. );
  418. it('generateVariantsForProduct generates variants', async () => {
  419. const result = await client.query<
  420. GenerateProductVariants.Mutation,
  421. GenerateProductVariants.Variables
  422. >(GENERATE_PRODUCT_VARIANTS, {
  423. productId: newProduct.id,
  424. defaultPrice: 123,
  425. defaultSku: 'ABC',
  426. });
  427. variants = result.generateVariantsForProduct.variants;
  428. expect(variants.length).toBe(2);
  429. expect(variants[0].options.length).toBe(1);
  430. expect(variants[1].options.length).toBe(1);
  431. });
  432. it('updateProductVariants updates variants', async () => {
  433. const firstVariant = variants[0];
  434. const result = await client.query<
  435. UpdateProductVariants.Mutation,
  436. UpdateProductVariants.Variables
  437. >(UPDATE_PRODUCT_VARIANTS, {
  438. input: [
  439. {
  440. id: firstVariant.id,
  441. translations: firstVariant.translations,
  442. sku: 'ABC',
  443. price: 432,
  444. },
  445. ],
  446. });
  447. const updatedVariant = result.updateProductVariants[0];
  448. if (!updatedVariant) {
  449. fail('no updated variant returned.');
  450. return;
  451. }
  452. expect(updatedVariant.sku).toBe('ABC');
  453. expect(updatedVariant.price).toBe(432);
  454. });
  455. it('updateProductVariants updates assets', async () => {
  456. const firstVariant = variants[0];
  457. const result = await client.query<
  458. UpdateProductVariants.Mutation,
  459. UpdateProductVariants.Variables
  460. >(UPDATE_PRODUCT_VARIANTS, {
  461. input: [
  462. {
  463. id: firstVariant.id,
  464. assetIds: ['T_1', 'T_2'],
  465. featuredAssetId: 'T_2',
  466. },
  467. ],
  468. });
  469. const updatedVariant = result.updateProductVariants[0];
  470. if (!updatedVariant) {
  471. fail('no updated variant returned.');
  472. return;
  473. }
  474. expect(updatedVariant.assets.map(a => a.id)).toEqual(['T_1', 'T_2']);
  475. expect(updatedVariant.featuredAsset!.id).toBe('T_2');
  476. });
  477. it('updateProductVariants updates taxCategory and priceBeforeTax', async () => {
  478. const firstVariant = variants[0];
  479. const result = await client.query<
  480. UpdateProductVariants.Mutation,
  481. UpdateProductVariants.Variables
  482. >(UPDATE_PRODUCT_VARIANTS, {
  483. input: [
  484. {
  485. id: firstVariant.id,
  486. price: 105,
  487. taxCategoryId: 'T_2',
  488. },
  489. ],
  490. });
  491. const updatedVariant = result.updateProductVariants[0];
  492. if (!updatedVariant) {
  493. fail('no updated variant returned.');
  494. return;
  495. }
  496. expect(updatedVariant.price).toBe(105);
  497. expect(updatedVariant.taxCategory.id).toBe('T_2');
  498. });
  499. it('updateProductVariants updates facetValues', async () => {
  500. const firstVariant = variants[0];
  501. const result = await client.query<
  502. UpdateProductVariants.Mutation,
  503. UpdateProductVariants.Variables
  504. >(UPDATE_PRODUCT_VARIANTS, {
  505. input: [
  506. {
  507. id: firstVariant.id,
  508. facetValueIds: ['T_1'],
  509. },
  510. ],
  511. });
  512. const updatedVariant = result.updateProductVariants[0];
  513. if (!updatedVariant) {
  514. fail('no updated variant returned.');
  515. return;
  516. }
  517. expect(updatedVariant.facetValues.length).toBe(1);
  518. expect(updatedVariant.facetValues[0].id).toBe('T_1');
  519. });
  520. it(
  521. 'updateProductVariants throws with an invalid variant id',
  522. assertThrowsWithMessage(
  523. () =>
  524. client.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
  525. UPDATE_PRODUCT_VARIANTS,
  526. {
  527. input: [
  528. {
  529. id: 'T_999',
  530. translations: variants[0].translations,
  531. sku: 'ABC',
  532. price: 432,
  533. },
  534. ],
  535. },
  536. ),
  537. `No ProductVariant with the id '999' could be found`,
  538. ),
  539. );
  540. });
  541. });
  542. describe('deletion', () => {
  543. let allProducts: GetProductList.Items[];
  544. let productToDelete: GetProductList.Items;
  545. beforeAll(async () => {
  546. const result = await client.query<GetProductList.Query>(GET_PRODUCT_LIST);
  547. allProducts = result.products.items;
  548. });
  549. it('deletes a product', async () => {
  550. productToDelete = allProducts[0];
  551. const result = await client.query(DELETE_PRODUCT, { id: productToDelete.id });
  552. expect(result.deleteProduct).toEqual({ result: DeletionResult.DELETED });
  553. });
  554. it('cannot get a deleted product', async () => {
  555. const result = await client.query<GetProductWithVariants.Query, GetProductWithVariants.Variables>(
  556. GET_PRODUCT_WITH_VARIANTS,
  557. {
  558. id: productToDelete.id,
  559. },
  560. );
  561. expect(result.product).toBe(null);
  562. });
  563. it('deleted product omitted from list', async () => {
  564. const result = await client.query<GetProductList.Query>(GET_PRODUCT_LIST);
  565. expect(result.products.items.length).toBe(allProducts.length - 1);
  566. expect(result.products.items.map(c => c.id).includes(productToDelete.id)).toBe(false);
  567. });
  568. it(
  569. 'updateProduct throws for deleted product',
  570. assertThrowsWithMessage(
  571. () =>
  572. client.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
  573. input: {
  574. id: productToDelete.id,
  575. facetValueIds: ['T_1'],
  576. },
  577. }),
  578. `No Product with the id '1' could be found`,
  579. ),
  580. );
  581. it(
  582. 'addOptionGroupToProduct throws for deleted product',
  583. assertThrowsWithMessage(
  584. () =>
  585. client.query<AddOptionGroupToProduct.Mutation, AddOptionGroupToProduct.Variables>(
  586. ADD_OPTION_GROUP_TO_PRODUCT,
  587. {
  588. optionGroupId: 'T_1',
  589. productId: productToDelete.id,
  590. },
  591. ),
  592. `No Product with the id '1' could be found`,
  593. ),
  594. );
  595. it(
  596. 'removeOptionGroupToProduct throws for deleted product',
  597. assertThrowsWithMessage(
  598. () =>
  599. client.query<
  600. RemoveOptionGroupFromProduct.Mutation,
  601. RemoveOptionGroupFromProduct.Variables
  602. >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
  603. optionGroupId: 'T_1',
  604. productId: productToDelete.id,
  605. }),
  606. `No Product with the id '1' could be found`,
  607. ),
  608. );
  609. it(
  610. 'generateVariantsForProduct throws for deleted product',
  611. assertThrowsWithMessage(
  612. () =>
  613. client.query<GenerateProductVariants.Mutation, GenerateProductVariants.Variables>(
  614. GENERATE_PRODUCT_VARIANTS,
  615. {
  616. productId: productToDelete.id,
  617. },
  618. ),
  619. `No Product with the id '1' could be found`,
  620. ),
  621. );
  622. });
  623. });
  624. const DELETE_PRODUCT = gql`
  625. mutation DeleteProduct($id: ID!) {
  626. deleteProduct(id: $id) {
  627. result
  628. }
  629. }
  630. `;