collection.e2e-spec.ts 46 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229
  1. /* tslint:disable:no-non-null-assertion */
  2. import { ROOT_COLLECTION_NAME } from '@vendure/common/lib/shared-constants';
  3. import gql from 'graphql-tag';
  4. import path from 'path';
  5. import { pick } from '../../common/lib/pick';
  6. import {
  7. facetValueCollectionFilter,
  8. variantNameCollectionFilter,
  9. } from '../src/config/collection/default-collection-filters';
  10. import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
  11. import { COLLECTION_FRAGMENT, FACET_VALUE_FRAGMENT } from './graphql/fragments';
  12. import {
  13. Collection,
  14. CreateCollection,
  15. CreateCollectionInput,
  16. CreateCollectionSelectVariants,
  17. DeleteCollection,
  18. DeleteProduct,
  19. DeletionResult,
  20. FacetValueFragment,
  21. GetAssetList,
  22. GetCollection,
  23. GetCollectionBreadcrumbs,
  24. GetCollectionProducts,
  25. GetCollections,
  26. GetCollectionsForProducts,
  27. GetCollectionsWithAssets,
  28. GetFacetValues,
  29. GetProductCollections,
  30. GetProductsWithVariantIds,
  31. LanguageCode,
  32. MoveCollection,
  33. SortOrder,
  34. UpdateCollection,
  35. UpdateProduct,
  36. UpdateProductVariants,
  37. } from './graphql/generated-e2e-admin-types';
  38. import {
  39. CREATE_COLLECTION,
  40. DELETE_PRODUCT,
  41. GET_ASSET_LIST,
  42. UPDATE_COLLECTION,
  43. UPDATE_PRODUCT,
  44. UPDATE_PRODUCT_VARIANTS,
  45. } from './graphql/shared-definitions';
  46. import { TestAdminClient } from './test-client';
  47. import { TestServer } from './test-server';
  48. import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
  49. import { awaitRunningJobs } from './utils/await-running-jobs';
  50. describe('Collection resolver', () => {
  51. const client = new TestAdminClient();
  52. const server = new TestServer();
  53. let assets: GetAssetList.Items[];
  54. let facetValues: FacetValueFragment[];
  55. let electronicsCollection: Collection.Fragment;
  56. let computersCollection: Collection.Fragment;
  57. let pearCollection: Collection.Fragment;
  58. beforeAll(async () => {
  59. const token = await server.init({
  60. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-collections.csv'),
  61. customerCount: 1,
  62. });
  63. await client.init();
  64. const assetsResult = await client.query<GetAssetList.Query, GetAssetList.Variables>(GET_ASSET_LIST, {
  65. options: {
  66. sort: {
  67. name: SortOrder.ASC,
  68. },
  69. },
  70. });
  71. assets = assetsResult.assets.items;
  72. const facetValuesResult = await client.query<GetFacetValues.Query>(GET_FACET_VALUES);
  73. facetValues = facetValuesResult.facets.items.reduce(
  74. (values, facet) => [...values, ...facet.values],
  75. [] as FacetValueFragment[],
  76. );
  77. }, TEST_SETUP_TIMEOUT_MS);
  78. afterAll(async () => {
  79. await server.destroy();
  80. });
  81. /**
  82. * Test case for https://github.com/vendure-ecommerce/vendure/issues/97
  83. */
  84. it('collection breadcrumbs works after bootstrap', async () => {
  85. const result = await client.query<GetCollectionBreadcrumbs.Query>(GET_COLLECTION_BREADCRUMBS, {
  86. id: 'T_1',
  87. });
  88. expect(result.collection!.breadcrumbs[0].name).toBe(ROOT_COLLECTION_NAME);
  89. });
  90. describe('createCollection', () => {
  91. it('creates a root collection', async () => {
  92. const result = await client.query<CreateCollection.Mutation, CreateCollection.Variables>(
  93. CREATE_COLLECTION,
  94. {
  95. input: {
  96. assetIds: [assets[0].id, assets[1].id],
  97. featuredAssetId: assets[1].id,
  98. filters: [
  99. {
  100. code: facetValueCollectionFilter.code,
  101. arguments: [
  102. {
  103. name: 'facetValueIds',
  104. value: `["${getFacetValueId('electronics')}"]`,
  105. type: 'facetValueIds',
  106. },
  107. {
  108. name: 'containsAny',
  109. value: `false`,
  110. type: 'boolean',
  111. },
  112. ],
  113. },
  114. ],
  115. translations: [
  116. { languageCode: LanguageCode.en, name: 'Electronics', description: '' },
  117. ],
  118. },
  119. },
  120. );
  121. electronicsCollection = result.createCollection;
  122. expect(electronicsCollection).toMatchSnapshot();
  123. expect(electronicsCollection.parent!.name).toBe(ROOT_COLLECTION_NAME);
  124. });
  125. it('creates a nested collection', async () => {
  126. const result = await client.query<CreateCollection.Mutation, CreateCollection.Variables>(
  127. CREATE_COLLECTION,
  128. {
  129. input: {
  130. parentId: electronicsCollection.id,
  131. translations: [{ languageCode: LanguageCode.en, name: 'Computers', description: '' }],
  132. filters: [
  133. {
  134. code: facetValueCollectionFilter.code,
  135. arguments: [
  136. {
  137. name: 'facetValueIds',
  138. value: `["${getFacetValueId('computers')}"]`,
  139. type: 'facetValueIds',
  140. },
  141. {
  142. name: 'containsAny',
  143. value: `false`,
  144. type: 'boolean',
  145. },
  146. ],
  147. },
  148. ],
  149. },
  150. },
  151. );
  152. computersCollection = result.createCollection;
  153. expect(computersCollection.parent!.name).toBe(electronicsCollection.name);
  154. });
  155. it('creates a 2nd level nested collection', async () => {
  156. const result = await client.query<CreateCollection.Mutation, CreateCollection.Variables>(
  157. CREATE_COLLECTION,
  158. {
  159. input: {
  160. parentId: computersCollection.id,
  161. translations: [{ languageCode: LanguageCode.en, name: 'Pear', description: '' }],
  162. filters: [
  163. {
  164. code: facetValueCollectionFilter.code,
  165. arguments: [
  166. {
  167. name: 'facetValueIds',
  168. value: `["${getFacetValueId('pear')}"]`,
  169. type: 'facetValueIds',
  170. },
  171. {
  172. name: 'containsAny',
  173. value: `false`,
  174. type: 'boolean',
  175. },
  176. ],
  177. },
  178. ],
  179. },
  180. },
  181. );
  182. pearCollection = result.createCollection;
  183. expect(pearCollection.parent!.name).toBe(computersCollection.name);
  184. });
  185. });
  186. describe('updateCollection', () => {
  187. it('updates with assets', async () => {
  188. const { updateCollection } = await client.query<
  189. UpdateCollection.Mutation,
  190. UpdateCollection.Variables
  191. >(UPDATE_COLLECTION, {
  192. input: {
  193. id: pearCollection.id,
  194. assetIds: [assets[1].id, assets[2].id],
  195. featuredAssetId: assets[1].id,
  196. translations: [{ languageCode: LanguageCode.en, description: 'Apple stuff ' }],
  197. },
  198. });
  199. expect(updateCollection).toMatchSnapshot();
  200. });
  201. it('updating existing assets', async () => {
  202. const { updateCollection } = await client.query<
  203. UpdateCollection.Mutation,
  204. UpdateCollection.Variables
  205. >(UPDATE_COLLECTION, {
  206. input: {
  207. id: pearCollection.id,
  208. assetIds: [assets[3].id, assets[0].id],
  209. featuredAssetId: assets[3].id,
  210. },
  211. });
  212. expect(updateCollection.assets.map(a => a.id)).toEqual([assets[3].id, assets[0].id]);
  213. });
  214. it('removes all assets', async () => {
  215. const { updateCollection } = await client.query<
  216. UpdateCollection.Mutation,
  217. UpdateCollection.Variables
  218. >(UPDATE_COLLECTION, {
  219. input: {
  220. id: pearCollection.id,
  221. assetIds: [],
  222. },
  223. });
  224. expect(updateCollection.assets).toEqual([]);
  225. expect(updateCollection.featuredAsset).toBeNull();
  226. });
  227. });
  228. it('collection query', async () => {
  229. const result = await client.query<GetCollection.Query, GetCollection.Variables>(GET_COLLECTION, {
  230. id: computersCollection.id,
  231. });
  232. if (!result.collection) {
  233. fail(`did not return the collection`);
  234. return;
  235. }
  236. expect(result.collection.id).toBe(computersCollection.id);
  237. });
  238. it('parent field', async () => {
  239. const result = await client.query<GetCollection.Query, GetCollection.Variables>(GET_COLLECTION, {
  240. id: computersCollection.id,
  241. });
  242. if (!result.collection) {
  243. fail(`did not return the collection`);
  244. return;
  245. }
  246. expect(result.collection.parent!.name).toBe('Electronics');
  247. });
  248. it('children field', async () => {
  249. const result = await client.query<GetCollection.Query, GetCollection.Variables>(GET_COLLECTION, {
  250. id: electronicsCollection.id,
  251. });
  252. if (!result.collection) {
  253. fail(`did not return the collection`);
  254. return;
  255. }
  256. expect(result.collection.children!.length).toBe(1);
  257. expect(result.collection.children![0].name).toBe('Computers');
  258. });
  259. it('breadcrumbs', async () => {
  260. const result = await client.query<GetCollectionBreadcrumbs.Query, GetCollectionBreadcrumbs.Variables>(
  261. GET_COLLECTION_BREADCRUMBS,
  262. {
  263. id: pearCollection.id,
  264. },
  265. );
  266. if (!result.collection) {
  267. fail(`did not return the collection`);
  268. return;
  269. }
  270. expect(result.collection.breadcrumbs).toEqual([
  271. { id: 'T_1', name: ROOT_COLLECTION_NAME },
  272. { id: electronicsCollection.id, name: electronicsCollection.name },
  273. { id: computersCollection.id, name: computersCollection.name },
  274. { id: pearCollection.id, name: pearCollection.name },
  275. ]);
  276. });
  277. it('breadcrumbs for root collection', async () => {
  278. const result = await client.query<GetCollectionBreadcrumbs.Query, GetCollectionBreadcrumbs.Variables>(
  279. GET_COLLECTION_BREADCRUMBS,
  280. {
  281. id: 'T_1',
  282. },
  283. );
  284. if (!result.collection) {
  285. fail(`did not return the collection`);
  286. return;
  287. }
  288. expect(result.collection.breadcrumbs).toEqual([{ id: 'T_1', name: ROOT_COLLECTION_NAME }]);
  289. });
  290. it('collections.assets', async () => {
  291. const { collections } = await client.query<GetCollectionsWithAssets.Query>(gql`
  292. query GetCollectionsWithAssets {
  293. collections {
  294. items {
  295. assets {
  296. name
  297. }
  298. }
  299. }
  300. }
  301. `);
  302. expect(collections.items[0].assets).toBeDefined();
  303. });
  304. describe('moveCollection', () => {
  305. it('moves a collection to a new parent', async () => {
  306. const result = await client.query<MoveCollection.Mutation, MoveCollection.Variables>(
  307. MOVE_COLLECTION,
  308. {
  309. input: {
  310. collectionId: pearCollection.id,
  311. parentId: electronicsCollection.id,
  312. index: 0,
  313. },
  314. },
  315. );
  316. expect(result.moveCollection.parent!.id).toBe(electronicsCollection.id);
  317. const positions = await getChildrenOf(electronicsCollection.id);
  318. expect(positions.map(i => i.id)).toEqual([pearCollection.id, computersCollection.id]);
  319. });
  320. it('re-evaluates Collection contents on move', async () => {
  321. const result = await client.query<GetCollectionProducts.Query, GetCollectionProducts.Variables>(
  322. GET_COLLECTION_PRODUCT_VARIANTS,
  323. { id: pearCollection.id },
  324. );
  325. expect(result.collection!.productVariants.items.map(i => i.name)).toEqual([
  326. 'Laptop 13 inch 8GB',
  327. 'Laptop 15 inch 8GB',
  328. 'Laptop 13 inch 16GB',
  329. 'Laptop 15 inch 16GB',
  330. 'Instant Camera',
  331. ]);
  332. });
  333. it('alters the position in the current parent 1', async () => {
  334. await client.query<MoveCollection.Mutation, MoveCollection.Variables>(MOVE_COLLECTION, {
  335. input: {
  336. collectionId: computersCollection.id,
  337. parentId: electronicsCollection.id,
  338. index: 0,
  339. },
  340. });
  341. const afterResult = await getChildrenOf(electronicsCollection.id);
  342. expect(afterResult.map(i => i.id)).toEqual([computersCollection.id, pearCollection.id]);
  343. });
  344. it('alters the position in the current parent 2', async () => {
  345. await client.query<MoveCollection.Mutation, MoveCollection.Variables>(MOVE_COLLECTION, {
  346. input: {
  347. collectionId: pearCollection.id,
  348. parentId: electronicsCollection.id,
  349. index: 0,
  350. },
  351. });
  352. const afterResult = await getChildrenOf(electronicsCollection.id);
  353. expect(afterResult.map(i => i.id)).toEqual([pearCollection.id, computersCollection.id]);
  354. });
  355. it('corrects an out-of-bounds negative index value', async () => {
  356. await client.query<MoveCollection.Mutation, MoveCollection.Variables>(MOVE_COLLECTION, {
  357. input: {
  358. collectionId: pearCollection.id,
  359. parentId: electronicsCollection.id,
  360. index: -3,
  361. },
  362. });
  363. const afterResult = await getChildrenOf(electronicsCollection.id);
  364. expect(afterResult.map(i => i.id)).toEqual([pearCollection.id, computersCollection.id]);
  365. });
  366. it('corrects an out-of-bounds positive index value', async () => {
  367. await client.query<MoveCollection.Mutation, MoveCollection.Variables>(MOVE_COLLECTION, {
  368. input: {
  369. collectionId: pearCollection.id,
  370. parentId: electronicsCollection.id,
  371. index: 10,
  372. },
  373. });
  374. const afterResult = await getChildrenOf(electronicsCollection.id);
  375. expect(afterResult.map(i => i.id)).toEqual([computersCollection.id, pearCollection.id]);
  376. });
  377. it(
  378. 'throws if attempting to move into self',
  379. assertThrowsWithMessage(
  380. () =>
  381. client.query<MoveCollection.Mutation, MoveCollection.Variables>(MOVE_COLLECTION, {
  382. input: {
  383. collectionId: pearCollection.id,
  384. parentId: pearCollection.id,
  385. index: 0,
  386. },
  387. }),
  388. `Cannot move a Collection into itself`,
  389. ),
  390. );
  391. it(
  392. 'throws if attempting to move into a decendant of self',
  393. assertThrowsWithMessage(
  394. () =>
  395. client.query<MoveCollection.Mutation, MoveCollection.Variables>(MOVE_COLLECTION, {
  396. input: {
  397. collectionId: pearCollection.id,
  398. parentId: pearCollection.id,
  399. index: 0,
  400. },
  401. }),
  402. `Cannot move a Collection into itself`,
  403. ),
  404. );
  405. async function getChildrenOf(parentId: string): Promise<Array<{ name: string; id: string }>> {
  406. const result = await client.query<GetCollections.Query>(GET_COLLECTIONS);
  407. return result.collections.items.filter(i => i.parent!.id === parentId);
  408. }
  409. });
  410. describe('deleteCollection', () => {
  411. let collectionToDelete: CreateCollection.CreateCollection;
  412. let laptopProductId: string;
  413. beforeAll(async () => {
  414. const { createCollection } = await client.query<
  415. CreateCollection.Mutation,
  416. CreateCollection.Variables
  417. >(CREATE_COLLECTION, {
  418. input: {
  419. filters: [
  420. {
  421. code: variantNameCollectionFilter.code,
  422. arguments: [
  423. {
  424. name: 'operator',
  425. value: 'contains',
  426. type: 'string',
  427. },
  428. {
  429. name: 'term',
  430. value: 'laptop',
  431. type: 'string',
  432. },
  433. ],
  434. },
  435. ],
  436. translations: [{ languageCode: LanguageCode.en, name: 'Delete Me', description: '' }],
  437. },
  438. });
  439. collectionToDelete = createCollection;
  440. });
  441. it(
  442. 'throws for invalid collection id',
  443. assertThrowsWithMessage(async () => {
  444. await client.query<DeleteCollection.Mutation, DeleteCollection.Variables>(DELETE_COLLECTION, {
  445. id: 'T_999',
  446. });
  447. }, "No Collection with the id '999' could be found"),
  448. );
  449. it('collection and product related prior to deletion', async () => {
  450. const { collection } = await client.query<
  451. GetCollectionProducts.Query,
  452. GetCollectionProducts.Variables
  453. >(GET_COLLECTION_PRODUCT_VARIANTS, {
  454. id: collectionToDelete.id,
  455. });
  456. expect(collection!.productVariants.items.map(pick(['name']))).toEqual([
  457. { name: 'Laptop 13 inch 8GB' },
  458. { name: 'Laptop 15 inch 8GB' },
  459. { name: 'Laptop 13 inch 16GB' },
  460. { name: 'Laptop 15 inch 16GB' },
  461. ]);
  462. laptopProductId = collection!.productVariants.items[0].productId;
  463. const { product } = await client.query<
  464. GetProductCollections.Query,
  465. GetProductCollections.Variables
  466. >(GET_PRODUCT_COLLECTIONS, {
  467. id: laptopProductId,
  468. });
  469. expect(product!.collections).toEqual([
  470. { id: 'T_3', name: 'Electronics' },
  471. { id: 'T_4', name: 'Computers' },
  472. { id: 'T_5', name: 'Pear' },
  473. { id: 'T_6', name: 'Delete Me' },
  474. ]);
  475. });
  476. it('deleteCollection works', async () => {
  477. const { deleteCollection } = await client.query<
  478. DeleteCollection.Mutation,
  479. DeleteCollection.Variables
  480. >(DELETE_COLLECTION, {
  481. id: collectionToDelete.id,
  482. });
  483. expect(deleteCollection.result).toBe(DeletionResult.DELETED);
  484. });
  485. it('deleted collection is null', async () => {
  486. const { collection } = await client.query<GetCollection.Query, GetCollection.Variables>(
  487. GET_COLLECTION,
  488. {
  489. id: collectionToDelete.id,
  490. },
  491. );
  492. expect(collection).toBeNull();
  493. });
  494. it('product no longer lists collection', async () => {
  495. const { product } = await client.query<
  496. GetProductCollections.Query,
  497. GetProductCollections.Variables
  498. >(GET_PRODUCT_COLLECTIONS, {
  499. id: laptopProductId,
  500. });
  501. expect(product!.collections).toEqual([
  502. { id: 'T_3', name: 'Electronics' },
  503. { id: 'T_4', name: 'Computers' },
  504. { id: 'T_5', name: 'Pear' },
  505. ]);
  506. });
  507. });
  508. describe('filters', () => {
  509. it('Collection with no filters has no productVariants', async () => {
  510. const result = await client.query<
  511. CreateCollectionSelectVariants.Mutation,
  512. CreateCollectionSelectVariants.Variables
  513. >(CREATE_COLLECTION_SELECT_VARIANTS, {
  514. input: {
  515. translations: [{ languageCode: LanguageCode.en, name: 'Empty', description: '' }],
  516. filters: [],
  517. } as CreateCollectionInput,
  518. });
  519. expect(result.createCollection.productVariants.totalItems).toBe(0);
  520. });
  521. describe('facetValue filter', () => {
  522. it('electronics', async () => {
  523. const result = await client.query<
  524. GetCollectionProducts.Query,
  525. GetCollectionProducts.Variables
  526. >(GET_COLLECTION_PRODUCT_VARIANTS, {
  527. id: electronicsCollection.id,
  528. });
  529. expect(result.collection!.productVariants.items.map(i => i.name)).toEqual([
  530. 'Laptop 13 inch 8GB',
  531. 'Laptop 15 inch 8GB',
  532. 'Laptop 13 inch 16GB',
  533. 'Laptop 15 inch 16GB',
  534. 'Curvy Monitor 24 inch',
  535. 'Curvy Monitor 27 inch',
  536. 'Gaming PC i7-8700 240GB SSD',
  537. 'Gaming PC R7-2700 240GB SSD',
  538. 'Gaming PC i7-8700 120GB SSD',
  539. 'Gaming PC R7-2700 120GB SSD',
  540. 'Hard Drive 1TB',
  541. 'Hard Drive 2TB',
  542. 'Hard Drive 3TB',
  543. 'Hard Drive 4TB',
  544. 'Hard Drive 6TB',
  545. 'Clacky Keyboard',
  546. 'USB Cable',
  547. 'Instant Camera',
  548. 'Camera Lens',
  549. 'Tripod',
  550. 'SLR Camera',
  551. ]);
  552. });
  553. it('computers', async () => {
  554. const result = await client.query<
  555. GetCollectionProducts.Query,
  556. GetCollectionProducts.Variables
  557. >(GET_COLLECTION_PRODUCT_VARIANTS, {
  558. id: computersCollection.id,
  559. });
  560. expect(result.collection!.productVariants.items.map(i => i.name)).toEqual([
  561. 'Laptop 13 inch 8GB',
  562. 'Laptop 15 inch 8GB',
  563. 'Laptop 13 inch 16GB',
  564. 'Laptop 15 inch 16GB',
  565. 'Curvy Monitor 24 inch',
  566. 'Curvy Monitor 27 inch',
  567. 'Gaming PC i7-8700 240GB SSD',
  568. 'Gaming PC R7-2700 240GB SSD',
  569. 'Gaming PC i7-8700 120GB SSD',
  570. 'Gaming PC R7-2700 120GB SSD',
  571. 'Hard Drive 1TB',
  572. 'Hard Drive 2TB',
  573. 'Hard Drive 3TB',
  574. 'Hard Drive 4TB',
  575. 'Hard Drive 6TB',
  576. 'Clacky Keyboard',
  577. 'USB Cable',
  578. ]);
  579. });
  580. it('photo AND pear', async () => {
  581. const result = await client.query<
  582. CreateCollectionSelectVariants.Mutation,
  583. CreateCollectionSelectVariants.Variables
  584. >(CREATE_COLLECTION_SELECT_VARIANTS, {
  585. input: {
  586. translations: [
  587. { languageCode: LanguageCode.en, name: 'Photo AND Pear', description: '' },
  588. ],
  589. filters: [
  590. {
  591. code: facetValueCollectionFilter.code,
  592. arguments: [
  593. {
  594. name: 'facetValueIds',
  595. value: `["${getFacetValueId('pear')}", "${getFacetValueId(
  596. 'photo',
  597. )}"]`,
  598. type: 'facetValueIds',
  599. },
  600. {
  601. name: 'containsAny',
  602. value: `false`,
  603. type: 'boolean',
  604. },
  605. ],
  606. },
  607. ],
  608. } as CreateCollectionInput,
  609. });
  610. await awaitRunningJobs(client);
  611. const { collection } = await client.query<GetCollection.Query, GetCollection.Variables>(
  612. GET_COLLECTION,
  613. {
  614. id: result.createCollection.id,
  615. },
  616. );
  617. expect(collection!.productVariants.items.map(i => i.name)).toEqual(['Instant Camera']);
  618. });
  619. it('photo OR pear', async () => {
  620. const result = await client.query<
  621. CreateCollectionSelectVariants.Mutation,
  622. CreateCollectionSelectVariants.Variables
  623. >(CREATE_COLLECTION_SELECT_VARIANTS, {
  624. input: {
  625. translations: [
  626. { languageCode: LanguageCode.en, name: 'Photo OR Pear', description: '' },
  627. ],
  628. filters: [
  629. {
  630. code: facetValueCollectionFilter.code,
  631. arguments: [
  632. {
  633. name: 'facetValueIds',
  634. value: `["${getFacetValueId('pear')}", "${getFacetValueId(
  635. 'photo',
  636. )}"]`,
  637. type: 'facetValueIds',
  638. },
  639. {
  640. name: 'containsAny',
  641. value: `true`,
  642. type: 'boolean',
  643. },
  644. ],
  645. },
  646. ],
  647. } as CreateCollectionInput,
  648. });
  649. await awaitRunningJobs(client);
  650. const { collection } = await client.query<GetCollection.Query, GetCollection.Variables>(
  651. GET_COLLECTION,
  652. {
  653. id: result.createCollection.id,
  654. },
  655. );
  656. expect(collection!.productVariants.items.map(i => i.name)).toEqual([
  657. 'Laptop 13 inch 8GB',
  658. 'Laptop 15 inch 8GB',
  659. 'Laptop 13 inch 16GB',
  660. 'Laptop 15 inch 16GB',
  661. 'Instant Camera',
  662. 'Camera Lens',
  663. 'Tripod',
  664. 'SLR Camera',
  665. 'Hat',
  666. ]);
  667. });
  668. it('bell OR pear in computers', async () => {
  669. const result = await client.query<
  670. CreateCollectionSelectVariants.Mutation,
  671. CreateCollectionSelectVariants.Variables
  672. >(CREATE_COLLECTION_SELECT_VARIANTS, {
  673. input: {
  674. parentId: computersCollection.id,
  675. translations: [
  676. {
  677. languageCode: LanguageCode.en,
  678. name: 'Bell OR Pear Computers',
  679. description: '',
  680. },
  681. ],
  682. filters: [
  683. {
  684. code: facetValueCollectionFilter.code,
  685. arguments: [
  686. {
  687. name: 'facetValueIds',
  688. value: `["${getFacetValueId('pear')}", "${getFacetValueId('bell')}"]`,
  689. type: 'facetValueIds',
  690. },
  691. {
  692. name: 'containsAny',
  693. value: `true`,
  694. type: 'boolean',
  695. },
  696. ],
  697. },
  698. ],
  699. } as CreateCollectionInput,
  700. });
  701. await awaitRunningJobs(client);
  702. const { collection } = await client.query<GetCollection.Query, GetCollection.Variables>(
  703. GET_COLLECTION,
  704. {
  705. id: result.createCollection.id,
  706. },
  707. );
  708. expect(collection!.productVariants.items.map(i => i.name)).toEqual([
  709. 'Laptop 13 inch 8GB',
  710. 'Laptop 15 inch 8GB',
  711. 'Laptop 13 inch 16GB',
  712. 'Laptop 15 inch 16GB',
  713. 'Curvy Monitor 24 inch',
  714. 'Curvy Monitor 27 inch',
  715. ]);
  716. });
  717. });
  718. describe('variantName filter', () => {
  719. async function createVariantNameFilteredCollection(
  720. operator: string,
  721. term: string,
  722. ): Promise<Collection.Fragment> {
  723. const { createCollection } = await client.query<
  724. CreateCollection.Mutation,
  725. CreateCollection.Variables
  726. >(CREATE_COLLECTION, {
  727. input: {
  728. translations: [
  729. { languageCode: LanguageCode.en, name: `${operator} ${term}`, description: '' },
  730. ],
  731. filters: [
  732. {
  733. code: variantNameCollectionFilter.code,
  734. arguments: [
  735. {
  736. name: 'operator',
  737. value: operator,
  738. type: 'string',
  739. },
  740. {
  741. name: 'term',
  742. value: term,
  743. type: 'string',
  744. },
  745. ],
  746. },
  747. ],
  748. },
  749. });
  750. return createCollection;
  751. }
  752. it('contains operator', async () => {
  753. const collection = await createVariantNameFilteredCollection('contains', 'camera');
  754. const result = await client.query<
  755. GetCollectionProducts.Query,
  756. GetCollectionProducts.Variables
  757. >(GET_COLLECTION_PRODUCT_VARIANTS, {
  758. id: collection.id,
  759. });
  760. expect(result.collection!.productVariants.items.map(i => i.name)).toEqual([
  761. 'Instant Camera',
  762. 'Camera Lens',
  763. 'SLR Camera',
  764. ]);
  765. });
  766. it('startsWith operator', async () => {
  767. const collection = await createVariantNameFilteredCollection('startsWith', 'camera');
  768. const result = await client.query<
  769. GetCollectionProducts.Query,
  770. GetCollectionProducts.Variables
  771. >(GET_COLLECTION_PRODUCT_VARIANTS, {
  772. id: collection.id,
  773. });
  774. expect(result.collection!.productVariants.items.map(i => i.name)).toEqual(['Camera Lens']);
  775. });
  776. it('endsWith operator', async () => {
  777. const collection = await createVariantNameFilteredCollection('endsWith', 'camera');
  778. const result = await client.query<
  779. GetCollectionProducts.Query,
  780. GetCollectionProducts.Variables
  781. >(GET_COLLECTION_PRODUCT_VARIANTS, {
  782. id: collection.id,
  783. });
  784. expect(result.collection!.productVariants.items.map(i => i.name)).toEqual([
  785. 'Instant Camera',
  786. 'SLR Camera',
  787. ]);
  788. });
  789. it('doesNotContain operator', async () => {
  790. const collection = await createVariantNameFilteredCollection('doesNotContain', 'camera');
  791. const result = await client.query<
  792. GetCollectionProducts.Query,
  793. GetCollectionProducts.Variables
  794. >(GET_COLLECTION_PRODUCT_VARIANTS, {
  795. id: collection.id,
  796. });
  797. expect(result.collection!.productVariants.items.map(i => i.name)).toEqual([
  798. 'Laptop 13 inch 8GB',
  799. 'Laptop 15 inch 8GB',
  800. 'Laptop 13 inch 16GB',
  801. 'Laptop 15 inch 16GB',
  802. 'Curvy Monitor 24 inch',
  803. 'Curvy Monitor 27 inch',
  804. 'Gaming PC i7-8700 240GB SSD',
  805. 'Gaming PC R7-2700 240GB SSD',
  806. 'Gaming PC i7-8700 120GB SSD',
  807. 'Gaming PC R7-2700 120GB SSD',
  808. 'Hard Drive 1TB',
  809. 'Hard Drive 2TB',
  810. 'Hard Drive 3TB',
  811. 'Hard Drive 4TB',
  812. 'Hard Drive 6TB',
  813. 'Clacky Keyboard',
  814. 'USB Cable',
  815. 'Tripod',
  816. 'Hat',
  817. ]);
  818. });
  819. });
  820. describe('re-evaluation of contents on changes', () => {
  821. let products: GetProductsWithVariantIds.Items[];
  822. beforeAll(async () => {
  823. const result = await client.query<GetProductsWithVariantIds.Query>(gql`
  824. query GetProductsWithVariantIds {
  825. products {
  826. items {
  827. id
  828. name
  829. variants {
  830. id
  831. }
  832. }
  833. }
  834. }
  835. `);
  836. products = result.products.items;
  837. });
  838. it('updates contents when Product is updated', async () => {
  839. await client.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
  840. input: {
  841. id: products[1].id,
  842. facetValueIds: [
  843. getFacetValueId('electronics'),
  844. getFacetValueId('computers'),
  845. getFacetValueId('pear'),
  846. ],
  847. },
  848. });
  849. await awaitRunningJobs(client);
  850. const result = await client.query<
  851. GetCollectionProducts.Query,
  852. GetCollectionProducts.Variables
  853. >(GET_COLLECTION_PRODUCT_VARIANTS, { id: pearCollection.id });
  854. expect(result.collection!.productVariants.items.map(i => i.name)).toEqual([
  855. 'Laptop 13 inch 8GB',
  856. 'Laptop 15 inch 8GB',
  857. 'Laptop 13 inch 16GB',
  858. 'Laptop 15 inch 16GB',
  859. 'Curvy Monitor 24 inch',
  860. 'Curvy Monitor 27 inch',
  861. 'Instant Camera',
  862. ]);
  863. });
  864. it('updates contents when ProductVariant is updated', async () => {
  865. const gamingPcFirstVariant = products.find(p => p.name === 'Gaming PC')!.variants[0];
  866. await client.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
  867. UPDATE_PRODUCT_VARIANTS,
  868. {
  869. input: [
  870. {
  871. id: gamingPcFirstVariant.id,
  872. facetValueIds: [getFacetValueId('pear')],
  873. },
  874. ],
  875. },
  876. );
  877. await awaitRunningJobs(client);
  878. const result = await client.query<
  879. GetCollectionProducts.Query,
  880. GetCollectionProducts.Variables
  881. >(GET_COLLECTION_PRODUCT_VARIANTS, { id: pearCollection.id });
  882. expect(result.collection!.productVariants.items.map(i => i.name)).toEqual([
  883. 'Laptop 13 inch 8GB',
  884. 'Laptop 15 inch 8GB',
  885. 'Laptop 13 inch 16GB',
  886. 'Laptop 15 inch 16GB',
  887. 'Curvy Monitor 24 inch',
  888. 'Curvy Monitor 27 inch',
  889. 'Gaming PC i7-8700 240GB SSD',
  890. 'Instant Camera',
  891. ]);
  892. });
  893. it('correctly filters when ProductVariant and Product both have matching FacetValue', async () => {
  894. const gamingPcFirstVariant = products.find(p => p.name === 'Gaming PC')!.variants[0];
  895. await client.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
  896. UPDATE_PRODUCT_VARIANTS,
  897. {
  898. input: [
  899. {
  900. id: gamingPcFirstVariant.id,
  901. facetValueIds: [getFacetValueId('electronics'), getFacetValueId('pear')],
  902. },
  903. ],
  904. },
  905. );
  906. const result = await client.query<
  907. GetCollectionProducts.Query,
  908. GetCollectionProducts.Variables
  909. >(GET_COLLECTION_PRODUCT_VARIANTS, { id: pearCollection.id });
  910. expect(result.collection!.productVariants.items.map(i => i.name)).toEqual([
  911. 'Laptop 13 inch 8GB',
  912. 'Laptop 15 inch 8GB',
  913. 'Laptop 13 inch 16GB',
  914. 'Laptop 15 inch 16GB',
  915. 'Curvy Monitor 24 inch',
  916. 'Curvy Monitor 27 inch',
  917. 'Gaming PC i7-8700 240GB SSD',
  918. 'Instant Camera',
  919. ]);
  920. });
  921. });
  922. it('filter inheritance of nested collections (issue #158)', async () => {
  923. const { createCollection: pearElectronics } = await client.query<
  924. CreateCollectionSelectVariants.Mutation,
  925. CreateCollectionSelectVariants.Variables
  926. >(CREATE_COLLECTION_SELECT_VARIANTS, {
  927. input: {
  928. parentId: electronicsCollection.id,
  929. translations: [
  930. { languageCode: LanguageCode.en, name: 'pear electronics', description: '' },
  931. ],
  932. filters: [
  933. {
  934. code: facetValueCollectionFilter.code,
  935. arguments: [
  936. {
  937. name: 'facetValueIds',
  938. value: `["${getFacetValueId('pear')}"]`,
  939. type: 'facetValueIds',
  940. },
  941. {
  942. name: 'containsAny',
  943. value: `false`,
  944. type: 'boolean',
  945. },
  946. ],
  947. },
  948. ],
  949. } as CreateCollectionInput,
  950. });
  951. await awaitRunningJobs(client);
  952. const result = await client.query<GetCollectionProducts.Query, GetCollectionProducts.Variables>(
  953. GET_COLLECTION_PRODUCT_VARIANTS,
  954. { id: pearElectronics.id },
  955. );
  956. expect(result.collection!.productVariants.items.map(i => i.name)).toEqual([
  957. 'Laptop 13 inch 8GB',
  958. 'Laptop 15 inch 8GB',
  959. 'Laptop 13 inch 16GB',
  960. 'Laptop 15 inch 16GB',
  961. 'Curvy Monitor 24 inch',
  962. 'Curvy Monitor 27 inch',
  963. 'Gaming PC i7-8700 240GB SSD',
  964. 'Instant Camera',
  965. // no "Hat"
  966. ]);
  967. });
  968. });
  969. describe('Product collections property', () => {
  970. it('returns all collections to which the Product belongs', async () => {
  971. const result = await client.query<
  972. GetCollectionsForProducts.Query,
  973. GetCollectionsForProducts.Variables
  974. >(GET_COLLECTIONS_FOR_PRODUCTS, { term: 'camera' });
  975. expect(result.products.items[0].collections).toEqual([
  976. { id: 'T_3', name: 'Electronics' },
  977. { id: 'T_5', name: 'Pear' },
  978. { id: 'T_8', name: 'Photo AND Pear' },
  979. { id: 'T_9', name: 'Photo OR Pear' },
  980. { id: 'T_11', name: 'contains camera' },
  981. { id: 'T_13', name: 'endsWith camera' },
  982. { id: 'T_15', name: 'pear electronics' },
  983. ]);
  984. });
  985. });
  986. it('collection does not list deleted products', async () => {
  987. await client.query<DeleteProduct.Mutation, DeleteProduct.Variables>(DELETE_PRODUCT, {
  988. id: 'T_2', // curvy monitor
  989. });
  990. const { collection } = await client.query<
  991. GetCollectionProducts.Query,
  992. GetCollectionProducts.Variables
  993. >(GET_COLLECTION_PRODUCT_VARIANTS, {
  994. id: pearCollection.id,
  995. });
  996. expect(collection!.productVariants.items.map(i => i.name)).toEqual([
  997. 'Laptop 13 inch 8GB',
  998. 'Laptop 15 inch 8GB',
  999. 'Laptop 13 inch 16GB',
  1000. 'Laptop 15 inch 16GB',
  1001. 'Gaming PC i7-8700 240GB SSD',
  1002. 'Instant Camera',
  1003. ]);
  1004. });
  1005. function getFacetValueId(code: string): string {
  1006. const match = facetValues.find(fv => fv.code === code);
  1007. if (!match) {
  1008. throw new Error(`Could not find a FacetValue with the code "${code}"`);
  1009. }
  1010. return match.id;
  1011. }
  1012. });
  1013. export const GET_COLLECTION = gql`
  1014. query GetCollection($id: ID!) {
  1015. collection(id: $id) {
  1016. ...Collection
  1017. productVariants {
  1018. items {
  1019. id
  1020. name
  1021. }
  1022. }
  1023. }
  1024. }
  1025. ${COLLECTION_FRAGMENT}
  1026. `;
  1027. export const MOVE_COLLECTION = gql`
  1028. mutation MoveCollection($input: MoveCollectionInput!) {
  1029. moveCollection(input: $input) {
  1030. ...Collection
  1031. }
  1032. }
  1033. ${COLLECTION_FRAGMENT}
  1034. `;
  1035. const GET_FACET_VALUES = gql`
  1036. query GetFacetValues {
  1037. facets {
  1038. items {
  1039. values {
  1040. ...FacetValue
  1041. }
  1042. }
  1043. }
  1044. }
  1045. ${FACET_VALUE_FRAGMENT}
  1046. `;
  1047. const GET_COLLECTIONS = gql`
  1048. query GetCollections {
  1049. collections {
  1050. items {
  1051. id
  1052. name
  1053. position
  1054. parent {
  1055. id
  1056. name
  1057. }
  1058. }
  1059. }
  1060. }
  1061. `;
  1062. const GET_COLLECTION_PRODUCT_VARIANTS = gql`
  1063. query GetCollectionProducts($id: ID!) {
  1064. collection(id: $id) {
  1065. productVariants {
  1066. items {
  1067. id
  1068. name
  1069. facetValues {
  1070. code
  1071. }
  1072. productId
  1073. }
  1074. }
  1075. }
  1076. }
  1077. `;
  1078. const CREATE_COLLECTION_SELECT_VARIANTS = gql`
  1079. mutation CreateCollectionSelectVariants($input: CreateCollectionInput!) {
  1080. createCollection(input: $input) {
  1081. id
  1082. productVariants {
  1083. items {
  1084. name
  1085. }
  1086. totalItems
  1087. }
  1088. }
  1089. }
  1090. `;
  1091. const GET_COLLECTION_BREADCRUMBS = gql`
  1092. query GetCollectionBreadcrumbs($id: ID!) {
  1093. collection(id: $id) {
  1094. breadcrumbs {
  1095. id
  1096. name
  1097. }
  1098. }
  1099. }
  1100. `;
  1101. const GET_COLLECTIONS_FOR_PRODUCTS = gql`
  1102. query GetCollectionsForProducts($term: String!) {
  1103. products(options: { filter: { name: { contains: $term } } }) {
  1104. items {
  1105. id
  1106. name
  1107. collections {
  1108. id
  1109. name
  1110. }
  1111. }
  1112. }
  1113. }
  1114. `;
  1115. const DELETE_COLLECTION = gql`
  1116. mutation DeleteCollection($id: ID!) {
  1117. deleteCollection(id: $id) {
  1118. result
  1119. message
  1120. }
  1121. }
  1122. `;
  1123. const GET_PRODUCT_COLLECTIONS = gql`
  1124. query GetProductCollections($id: ID!) {
  1125. product(id: $id) {
  1126. id
  1127. collections {
  1128. id
  1129. name
  1130. }
  1131. }
  1132. }
  1133. `;