collection.e2e-spec.ts 48 KB

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