collection.e2e-spec.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  1. /* tslint:disable:no-non-null-assertion */
  2. import {
  3. Collection,
  4. ConfigArgType,
  5. CreateCollection,
  6. CreateCollectionInput,
  7. FacetValue,
  8. GetAssetList,
  9. GetCollection,
  10. LanguageCode,
  11. MoveCollection,
  12. ProductWithVariants,
  13. SortOrder,
  14. UpdateCollection,
  15. UpdateProduct,
  16. UpdateProductVariants,
  17. } from '@vendure/common/lib/generated-types';
  18. import { ROOT_COLLECTION_NAME } from '@vendure/common/lib/shared-constants';
  19. import gql from 'graphql-tag';
  20. import path from 'path';
  21. import {
  22. CREATE_COLLECTION,
  23. GET_COLLECTION,
  24. MOVE_COLLECTION,
  25. UPDATE_COLLECTION,
  26. } from '../../../admin-ui/src/app/data/definitions/collection-definitions';
  27. import { FACET_VALUE_FRAGMENT } from '../../../admin-ui/src/app/data/definitions/facet-definitions';
  28. import { GET_ASSET_LIST, UPDATE_PRODUCT, UPDATE_PRODUCT_VARIANTS } from '../../../admin-ui/src/app/data/definitions/product-definitions';
  29. import { facetValueCollectionFilter } from '../src/config/collection/default-collection-filters';
  30. import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
  31. import { TestAdminClient } from './test-client';
  32. import { TestServer } from './test-server';
  33. import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
  34. describe('Collection resolver', () => {
  35. const client = new TestAdminClient();
  36. const server = new TestServer();
  37. let assets: GetAssetList.Items[];
  38. let facetValues: FacetValue.Fragment[];
  39. let electronicsCollection: Collection.Fragment;
  40. let computersCollection: Collection.Fragment;
  41. let pearCollection: Collection.Fragment;
  42. beforeAll(async () => {
  43. const token = await server.init({
  44. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-collections.csv'),
  45. customerCount: 1,
  46. });
  47. await client.init();
  48. const assetsResult = await client.query<GetAssetList.Query, GetAssetList.Variables>(GET_ASSET_LIST, {
  49. options: {
  50. sort: {
  51. name: SortOrder.ASC,
  52. },
  53. },
  54. });
  55. assets = assetsResult.assets.items;
  56. const facetValuesResult = await client.query(GET_FACET_VALUES);
  57. facetValues = facetValuesResult.facets.items.reduce(
  58. (values: any, facet: any) => [...values, ...facet.values],
  59. [],
  60. );
  61. }, TEST_SETUP_TIMEOUT_MS);
  62. afterAll(async () => {
  63. await server.destroy();
  64. });
  65. describe('createCollection', () => {
  66. it('creates a root collection', async () => {
  67. const result = await client.query<CreateCollection.Mutation, CreateCollection.Variables>(
  68. CREATE_COLLECTION,
  69. {
  70. input: {
  71. assetIds: [assets[0].id, assets[1].id],
  72. featuredAssetId: assets[1].id,
  73. filters: [
  74. {
  75. code: facetValueCollectionFilter.code,
  76. arguments: [
  77. {
  78. name: 'facetValueIds',
  79. value: `["${getFacetValueId('electronics')}"]`,
  80. type: ConfigArgType.FACET_VALUE_IDS,
  81. },
  82. ],
  83. },
  84. ],
  85. translations: [
  86. { languageCode: LanguageCode.en, name: 'Electronics', description: '' },
  87. ],
  88. },
  89. },
  90. );
  91. electronicsCollection = result.createCollection;
  92. expect(electronicsCollection).toMatchSnapshot();
  93. expect(electronicsCollection.parent!.name).toBe(ROOT_COLLECTION_NAME);
  94. });
  95. it('creates a nested category', async () => {
  96. const result = await client.query<CreateCollection.Mutation, CreateCollection.Variables>(
  97. CREATE_COLLECTION,
  98. {
  99. input: {
  100. parentId: electronicsCollection.id,
  101. translations: [{ languageCode: LanguageCode.en, name: 'Computers', description: '' }],
  102. filters: [
  103. {
  104. code: facetValueCollectionFilter.code,
  105. arguments: [
  106. {
  107. name: 'facetValueIds',
  108. value: `["${getFacetValueId('computers')}"]`,
  109. type: ConfigArgType.FACET_VALUE_IDS,
  110. },
  111. ],
  112. },
  113. ],
  114. },
  115. },
  116. );
  117. computersCollection = result.createCollection;
  118. expect(computersCollection.parent!.name).toBe(electronicsCollection.name);
  119. });
  120. it('creates a 2nd level nested category', async () => {
  121. const result = await client.query<CreateCollection.Mutation, CreateCollection.Variables>(
  122. CREATE_COLLECTION,
  123. {
  124. input: {
  125. parentId: computersCollection.id,
  126. translations: [{ languageCode: LanguageCode.en, name: 'Pear', description: '' }],
  127. filters: [
  128. {
  129. code: facetValueCollectionFilter.code,
  130. arguments: [
  131. {
  132. name: 'facetValueIds',
  133. value: `["${getFacetValueId('pear')}"]`,
  134. type: ConfigArgType.FACET_VALUE_IDS,
  135. },
  136. ],
  137. },
  138. ],
  139. },
  140. },
  141. );
  142. pearCollection = result.createCollection;
  143. expect(pearCollection.parent!.name).toBe(computersCollection.name);
  144. });
  145. });
  146. it('collection query', async () => {
  147. const result = await client.query<GetCollection.Query, GetCollection.Variables>(GET_COLLECTION, {
  148. id: computersCollection.id,
  149. });
  150. if (!result.collection) {
  151. fail(`did not return the collection`);
  152. return;
  153. }
  154. expect(result.collection.id).toBe(computersCollection.id);
  155. });
  156. it('breadcrumbs', async () => {
  157. const result = await client.query(GET_COLLECTION_BREADCRUMBS, {
  158. id: pearCollection.id,
  159. });
  160. if (!result.collection) {
  161. fail(`did not return the collection`);
  162. return;
  163. }
  164. expect(result.collection.breadcrumbs).toEqual([
  165. { id: 'T_1', name: ROOT_COLLECTION_NAME },
  166. { id: electronicsCollection.id, name: electronicsCollection.name },
  167. { id: computersCollection.id, name: computersCollection.name },
  168. { id: pearCollection.id, name: pearCollection.name },
  169. ]);
  170. });
  171. it('breadcrumbs for root collection', async () => {
  172. const result = await client.query(GET_COLLECTION_BREADCRUMBS, {
  173. id: 'T_1',
  174. });
  175. if (!result.collection) {
  176. fail(`did not return the collection`);
  177. return;
  178. }
  179. expect(result.collection.breadcrumbs).toEqual([{ id: 'T_1', name: ROOT_COLLECTION_NAME }]);
  180. });
  181. it('updateCollection', async () => {
  182. const result = await client.query<UpdateCollection.Mutation, UpdateCollection.Variables>(
  183. UPDATE_COLLECTION,
  184. {
  185. input: {
  186. id: pearCollection.id,
  187. assetIds: [assets[1].id],
  188. featuredAssetId: assets[1].id,
  189. translations: [{ languageCode: LanguageCode.en, description: 'Apple stuff ' }],
  190. },
  191. },
  192. );
  193. expect(result.updateCollection).toMatchSnapshot();
  194. });
  195. describe('moveCollection', () => {
  196. it('moves a collection to a new parent', async () => {
  197. const result = await client.query<MoveCollection.Mutation, MoveCollection.Variables>(
  198. MOVE_COLLECTION,
  199. {
  200. input: {
  201. collectionId: pearCollection.id,
  202. parentId: electronicsCollection.id,
  203. index: 0,
  204. },
  205. },
  206. );
  207. expect(result.moveCollection.parent!.id).toBe(electronicsCollection.id);
  208. const positions = await getChildrenOf(electronicsCollection.id);
  209. expect(positions.map((i: any) => i.id)).toEqual([pearCollection.id, computersCollection.id]);
  210. });
  211. it('re-evaluates Collection contents on move', async () => {
  212. const result = await client.query(GET_COLLECTION_PRODUCT_VARIANTS, { id: pearCollection.id });
  213. expect(result.collection.productVariants.items.map((i: any) => i.name)).toEqual([
  214. 'Laptop 13 inch 8GB',
  215. 'Laptop 15 inch 8GB',
  216. 'Laptop 13 inch 16GB',
  217. 'Laptop 15 inch 16GB',
  218. 'Instant Camera',
  219. ]);
  220. });
  221. it('alters the position in the current parent', async () => {
  222. await client.query<MoveCollection.Mutation, MoveCollection.Variables>(MOVE_COLLECTION, {
  223. input: {
  224. collectionId: pearCollection.id,
  225. parentId: electronicsCollection.id,
  226. index: 1,
  227. },
  228. });
  229. const afterResult = await getChildrenOf(electronicsCollection.id);
  230. expect(afterResult.map((i: any) => i.id)).toEqual([computersCollection.id, pearCollection.id]);
  231. });
  232. it('corrects an out-of-bounds negative index value', async () => {
  233. await client.query<MoveCollection.Mutation, MoveCollection.Variables>(MOVE_COLLECTION, {
  234. input: {
  235. collectionId: pearCollection.id,
  236. parentId: electronicsCollection.id,
  237. index: -3,
  238. },
  239. });
  240. const afterResult = await getChildrenOf(electronicsCollection.id);
  241. expect(afterResult.map((i: any) => i.id)).toEqual([pearCollection.id, computersCollection.id]);
  242. });
  243. it('corrects an out-of-bounds positive index value', async () => {
  244. await client.query<MoveCollection.Mutation, MoveCollection.Variables>(MOVE_COLLECTION, {
  245. input: {
  246. collectionId: pearCollection.id,
  247. parentId: electronicsCollection.id,
  248. index: 10,
  249. },
  250. });
  251. const afterResult = await getChildrenOf(electronicsCollection.id);
  252. expect(afterResult.map((i: any) => i.id)).toEqual([computersCollection.id, pearCollection.id]);
  253. });
  254. it(
  255. 'throws if attempting to move into self',
  256. assertThrowsWithMessage(
  257. () =>
  258. client.query<MoveCollection.Mutation, MoveCollection.Variables>(MOVE_COLLECTION, {
  259. input: {
  260. collectionId: pearCollection.id,
  261. parentId: pearCollection.id,
  262. index: 0,
  263. },
  264. }),
  265. `Cannot move a Collection into itself`,
  266. ),
  267. );
  268. it(
  269. 'throws if attempting to move into a decendant of self',
  270. assertThrowsWithMessage(
  271. () =>
  272. client.query<MoveCollection.Mutation, MoveCollection.Variables>(MOVE_COLLECTION, {
  273. input: {
  274. collectionId: pearCollection.id,
  275. parentId: pearCollection.id,
  276. index: 0,
  277. },
  278. }),
  279. `Cannot move a Collection into itself`,
  280. ),
  281. );
  282. async function getChildrenOf(parentId: string): Promise<Array<{ name: string; id: string }>> {
  283. const result = await client.query(GET_COLLECTIONS);
  284. return result.collections.items.filter((i: any) => i.parent.id === parentId);
  285. }
  286. });
  287. describe('filters', () => {
  288. it('Collection with no filters has no productVariants', async () => {
  289. const result = await client.query(CREATE_COLLECTION_SELECT_VARIANTS, {
  290. input: {
  291. translations: [{ languageCode: LanguageCode.en, name: 'Empty', description: '' }],
  292. filters: [],
  293. } as CreateCollectionInput,
  294. });
  295. expect(result.createCollection.productVariants.totalItems).toBe(0);
  296. });
  297. describe('facetValue filter', () => {
  298. it('electronics', async () => {
  299. const result = await client.query(GET_COLLECTION_PRODUCT_VARIANTS, {
  300. id: electronicsCollection.id,
  301. });
  302. expect(result.collection.productVariants.items.map((i: any) => i.name)).toEqual([
  303. 'Laptop 13 inch 8GB',
  304. 'Laptop 15 inch 8GB',
  305. 'Laptop 13 inch 16GB',
  306. 'Laptop 15 inch 16GB',
  307. 'Curvy Monitor 24 inch',
  308. 'Curvy Monitor 27 inch',
  309. 'Gaming PC i7-8700 240GB SSD',
  310. 'Gaming PC R7-2700 240GB SSD',
  311. 'Gaming PC i7-8700 120GB SSD',
  312. 'Gaming PC R7-2700 120GB SSD',
  313. 'Hard Drive 1TB',
  314. 'Hard Drive 2TB',
  315. 'Hard Drive 3TB',
  316. 'Hard Drive 4TB',
  317. 'Hard Drive 6TB',
  318. 'Clacky Keyboard',
  319. 'USB Cable',
  320. 'Instant Camera',
  321. 'Camera Lens',
  322. 'Tripod',
  323. 'SLR Camera',
  324. ]);
  325. });
  326. it('computers', async () => {
  327. const result = await client.query(GET_COLLECTION_PRODUCT_VARIANTS, {
  328. id: computersCollection.id,
  329. });
  330. expect(result.collection.productVariants.items.map((i: any) => i.name)).toEqual([
  331. 'Laptop 13 inch 8GB',
  332. 'Laptop 15 inch 8GB',
  333. 'Laptop 13 inch 16GB',
  334. 'Laptop 15 inch 16GB',
  335. 'Curvy Monitor 24 inch',
  336. 'Curvy Monitor 27 inch',
  337. 'Gaming PC i7-8700 240GB SSD',
  338. 'Gaming PC R7-2700 240GB SSD',
  339. 'Gaming PC i7-8700 120GB SSD',
  340. 'Gaming PC R7-2700 120GB SSD',
  341. 'Hard Drive 1TB',
  342. 'Hard Drive 2TB',
  343. 'Hard Drive 3TB',
  344. 'Hard Drive 4TB',
  345. 'Hard Drive 6TB',
  346. 'Clacky Keyboard',
  347. 'USB Cable',
  348. ]);
  349. });
  350. it('photo and pear', async () => {
  351. const result = await client.query(CREATE_COLLECTION_SELECT_VARIANTS, {
  352. input: {
  353. translations: [
  354. { languageCode: LanguageCode.en, name: 'Photo Pear', description: '' },
  355. ],
  356. filters: [
  357. {
  358. code: facetValueCollectionFilter.code,
  359. arguments: [
  360. {
  361. name: 'facetValueIds',
  362. value: `["${getFacetValueId('pear')}", "${getFacetValueId(
  363. 'photo',
  364. )}"]`,
  365. type: ConfigArgType.FACET_VALUE_IDS,
  366. },
  367. ],
  368. },
  369. ],
  370. } as CreateCollectionInput,
  371. });
  372. expect(result.createCollection.productVariants.items.map((i: any) => i.name)).toEqual([
  373. 'Instant Camera',
  374. ]);
  375. });
  376. });
  377. describe('re-evaluation of contents on changes', () => {
  378. let products: ProductWithVariants.Fragment[];
  379. beforeAll(async () => {
  380. const result = await client.query(gql`
  381. query {
  382. products {
  383. items {
  384. id
  385. name
  386. variants {
  387. id
  388. }
  389. }
  390. }
  391. }
  392. `);
  393. products = result.products.items;
  394. });
  395. it('updates contents when Product is updated', async () => {
  396. await client.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
  397. input: {
  398. id: products[1].id,
  399. facetValueIds: [
  400. getFacetValueId('electronics'),
  401. getFacetValueId('computers'),
  402. getFacetValueId('pear'),
  403. ],
  404. },
  405. });
  406. const result = await client.query(GET_COLLECTION_PRODUCT_VARIANTS, { id: pearCollection.id });
  407. expect(result.collection.productVariants.items.map((i: any) => i.name)).toEqual([
  408. 'Laptop 13 inch 8GB',
  409. 'Laptop 15 inch 8GB',
  410. 'Laptop 13 inch 16GB',
  411. 'Laptop 15 inch 16GB',
  412. 'Curvy Monitor 24 inch',
  413. 'Curvy Monitor 27 inch',
  414. 'Instant Camera',
  415. ]);
  416. });
  417. it('updates contents when ProductVariant is updated', async () => {
  418. const gamingPcFirstVariant = products.find(p => p.name === 'Gaming PC')!.variants[0];
  419. await client.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
  420. UPDATE_PRODUCT_VARIANTS,
  421. {
  422. input: [
  423. {
  424. id: gamingPcFirstVariant.id,
  425. facetValueIds: [getFacetValueId('pear')],
  426. },
  427. ],
  428. },
  429. );
  430. const result = await client.query(GET_COLLECTION_PRODUCT_VARIANTS, { id: pearCollection.id });
  431. expect(result.collection.productVariants.items.map((i: any) => i.name)).toEqual([
  432. 'Laptop 13 inch 8GB',
  433. 'Laptop 15 inch 8GB',
  434. 'Laptop 13 inch 16GB',
  435. 'Laptop 15 inch 16GB',
  436. 'Curvy Monitor 24 inch',
  437. 'Curvy Monitor 27 inch',
  438. 'Gaming PC i7-8700 240GB SSD',
  439. 'Instant Camera',
  440. ]);
  441. });
  442. it('correctly filters when ProductVariant and Product both have matching FacetValue', async () => {
  443. const gamingPcFirstVariant = products.find(p => p.name === 'Gaming PC')!.variants[0];
  444. await client.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
  445. UPDATE_PRODUCT_VARIANTS,
  446. {
  447. input: [
  448. {
  449. id: gamingPcFirstVariant.id,
  450. facetValueIds: [getFacetValueId('electronics'), getFacetValueId('pear')],
  451. },
  452. ],
  453. },
  454. );
  455. const result = await client.query(GET_COLLECTION_PRODUCT_VARIANTS, { id: pearCollection.id });
  456. expect(result.collection.productVariants.items.map((i: any) => i.name)).toEqual([
  457. 'Laptop 13 inch 8GB',
  458. 'Laptop 15 inch 8GB',
  459. 'Laptop 13 inch 16GB',
  460. 'Laptop 15 inch 16GB',
  461. 'Curvy Monitor 24 inch',
  462. 'Curvy Monitor 27 inch',
  463. 'Gaming PC i7-8700 240GB SSD',
  464. 'Instant Camera',
  465. ]);
  466. });
  467. });
  468. });
  469. describe('Product collections property', () => {
  470. it('returns all collections to which the Product belongs', async () => {
  471. const result = await client.query(GET_COLLECTIONS_FOR_PRODUCTS, { term: 'camera' });
  472. expect(result.products.items[0].collections).toEqual([
  473. { id: 'T_3', name: 'Electronics' },
  474. { id: 'T_5', name: 'Pear' },
  475. { id: 'T_7', name: 'Photo Pear' },
  476. ]);
  477. });
  478. });
  479. function getFacetValueId(code: string): string {
  480. const match = facetValues.find(fv => fv.code === code);
  481. if (!match) {
  482. throw new Error(`Could not find a FacetValue with the code "${code}"`);
  483. }
  484. return match.id;
  485. }
  486. });
  487. const GET_FACET_VALUES = gql`
  488. query {
  489. facets {
  490. items {
  491. values {
  492. ...FacetValue
  493. }
  494. }
  495. }
  496. }
  497. ${FACET_VALUE_FRAGMENT}
  498. `;
  499. const GET_COLLECTIONS = gql`
  500. query GetCollections {
  501. collections(languageCode: en) {
  502. items {
  503. id
  504. name
  505. position
  506. parent {
  507. id
  508. name
  509. }
  510. }
  511. }
  512. }
  513. `;
  514. const GET_COLLECTION_PRODUCT_VARIANTS = gql`
  515. query GetCollectionProducts($id: ID!) {
  516. collection(id: $id) {
  517. productVariants {
  518. items {
  519. id
  520. name
  521. facetValues {
  522. code
  523. }
  524. }
  525. }
  526. }
  527. }
  528. `;
  529. const CREATE_COLLECTION_SELECT_VARIANTS = gql`
  530. mutation($input: CreateCollectionInput!) {
  531. createCollection(input: $input) {
  532. productVariants {
  533. items {
  534. name
  535. }
  536. totalItems
  537. }
  538. }
  539. }
  540. `;
  541. const GET_COLLECTION_BREADCRUMBS = gql`
  542. query GetCollectionBreadcrumbs($id: ID!) {
  543. collection(id: $id) {
  544. breadcrumbs {
  545. id
  546. name
  547. }
  548. }
  549. }
  550. `;
  551. const GET_COLLECTIONS_FOR_PRODUCTS = gql`
  552. query GetCollectionsForProducts($term: String!) {
  553. products(options: { filter: { name: { contains: $term } } }) {
  554. items {
  555. id
  556. name
  557. collections {
  558. id
  559. name
  560. }
  561. }
  562. }
  563. }
  564. `;