product.e2e-spec.ts 26 KB

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