elasticsearch-plugin.e2e-spec.ts 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021
  1. /* tslint:disable:no-non-null-assertion no-console */
  2. import { Client } from '@elastic/elasticsearch';
  3. import { SortOrder } from '@vendure/common/lib/generated-types';
  4. import { pick } from '@vendure/common/lib/pick';
  5. import {
  6. DefaultJobQueuePlugin,
  7. DefaultLogger,
  8. facetValueCollectionFilter,
  9. Logger,
  10. LogLevel,
  11. mergeConfig,
  12. } from '@vendure/core';
  13. import { createTestEnvironment, E2E_DEFAULT_CHANNEL_TOKEN, SimpleGraphQLClient } from '@vendure/testing';
  14. import gql from 'graphql-tag';
  15. import path from 'path';
  16. import { initialData } from '../../../e2e-common/e2e-initial-data';
  17. import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
  18. import {
  19. AssignProductsToChannel,
  20. CreateChannel,
  21. CreateCollection,
  22. CreateFacet,
  23. CurrencyCode,
  24. DeleteAsset,
  25. DeleteProduct,
  26. DeleteProductVariant,
  27. LanguageCode,
  28. RemoveProductsFromChannel,
  29. SearchFacetValues,
  30. SearchGetPrices,
  31. SearchInput,
  32. UpdateAsset,
  33. UpdateCollection,
  34. UpdateProduct,
  35. UpdateProductVariants,
  36. UpdateTaxRate,
  37. } from '../../core/e2e/graphql/generated-e2e-admin-types';
  38. import { LogicalOperator, SearchProductsShop } from '../../core/e2e/graphql/generated-e2e-shop-types';
  39. import {
  40. ASSIGN_PRODUCT_TO_CHANNEL,
  41. CREATE_CHANNEL,
  42. CREATE_COLLECTION,
  43. CREATE_FACET,
  44. DELETE_ASSET,
  45. DELETE_PRODUCT,
  46. DELETE_PRODUCT_VARIANT,
  47. REMOVE_PRODUCT_FROM_CHANNEL,
  48. UPDATE_ASSET,
  49. UPDATE_COLLECTION,
  50. UPDATE_PRODUCT,
  51. UPDATE_PRODUCT_VARIANTS,
  52. UPDATE_TAX_RATE,
  53. } from '../../core/e2e/graphql/shared-definitions';
  54. import { SEARCH_PRODUCTS_SHOP } from '../../core/e2e/graphql/shop-definitions';
  55. import { awaitRunningJobs } from '../../core/e2e/utils/await-running-jobs';
  56. import { loggerCtx } from '../src/constants';
  57. import { ElasticsearchPlugin } from '../src/plugin';
  58. // tslint:disable-next-line:no-var-requires
  59. const { elasticsearchHost, elasticsearchPort } = require('./constants');
  60. import {
  61. GetJobInfo,
  62. JobState,
  63. Reindex,
  64. SearchProductsAdmin,
  65. } from './graphql/generated-e2e-elasticsearch-plugin-types';
  66. /**
  67. * The Elasticsearch tests sometimes take a long time in CI due to limited resources.
  68. * We increase the timeout to 30 seconds to prevent failure due to timeouts.
  69. */
  70. if (process.env.CI) {
  71. jest.setTimeout(10 * 3000);
  72. }
  73. describe('Elasticsearch plugin', () => {
  74. const { server, adminClient, shopClient } = createTestEnvironment(
  75. mergeConfig(testConfig, {
  76. apiOptions: {
  77. port: 4050,
  78. },
  79. workerOptions: {
  80. options: {
  81. port: 4055,
  82. },
  83. },
  84. logger: new DefaultLogger({ level: LogLevel.Info }),
  85. plugins: [
  86. ElasticsearchPlugin.init({
  87. indexPrefix: 'e2e-tests',
  88. port: elasticsearchPort,
  89. host: elasticsearchHost,
  90. }),
  91. DefaultJobQueuePlugin,
  92. ],
  93. }),
  94. );
  95. beforeAll(async () => {
  96. await server.init({
  97. initialData,
  98. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
  99. customerCount: 1,
  100. });
  101. await adminClient.asSuperAdmin();
  102. await adminClient.query(REINDEX);
  103. await awaitRunningJobs(adminClient);
  104. }, TEST_SETUP_TIMEOUT_MS);
  105. afterAll(async () => {
  106. await server.destroy();
  107. });
  108. function doAdminSearchQuery(input: SearchInput) {
  109. return adminClient.query<SearchProductsAdmin.Query, SearchProductsAdmin.Variables>(SEARCH_PRODUCTS, {
  110. input,
  111. });
  112. }
  113. async function testGroupByProduct(client: SimpleGraphQLClient) {
  114. const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  115. SEARCH_PRODUCTS_SHOP,
  116. {
  117. input: {
  118. groupByProduct: true,
  119. },
  120. },
  121. );
  122. expect(result.search.totalItems).toBe(20);
  123. }
  124. async function testNoGrouping(client: SimpleGraphQLClient) {
  125. const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  126. SEARCH_PRODUCTS_SHOP,
  127. {
  128. input: {
  129. groupByProduct: false,
  130. },
  131. },
  132. );
  133. expect(result.search.totalItems).toBe(34);
  134. }
  135. async function testMatchSearchTerm(client: SimpleGraphQLClient) {
  136. const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  137. SEARCH_PRODUCTS_SHOP,
  138. {
  139. input: {
  140. term: 'camera',
  141. groupByProduct: true,
  142. },
  143. },
  144. );
  145. expect(result.search.items.map(i => i.productName)).toEqual([
  146. 'Instant Camera',
  147. 'Camera Lens',
  148. 'SLR Camera',
  149. ]);
  150. }
  151. async function testMatchFacetIdsAnd(client: SimpleGraphQLClient) {
  152. const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  153. SEARCH_PRODUCTS_SHOP,
  154. {
  155. input: {
  156. facetValueIds: ['T_1', 'T_2'],
  157. facetValueOperator: LogicalOperator.AND,
  158. groupByProduct: true,
  159. sort: {
  160. name: SortOrder.ASC,
  161. },
  162. },
  163. },
  164. );
  165. expect(result.search.items.map(i => i.productName)).toEqual([
  166. 'Clacky Keyboard',
  167. 'Curvy Monitor',
  168. 'Gaming PC',
  169. 'Hard Drive',
  170. 'Laptop',
  171. 'USB Cable',
  172. ]);
  173. }
  174. async function testMatchFacetIdsOr(client: SimpleGraphQLClient) {
  175. const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  176. SEARCH_PRODUCTS_SHOP,
  177. {
  178. input: {
  179. facetValueIds: ['T_1', 'T_5'],
  180. facetValueOperator: LogicalOperator.OR,
  181. groupByProduct: true,
  182. sort: {
  183. name: SortOrder.ASC,
  184. },
  185. take: 20,
  186. },
  187. },
  188. );
  189. expect(result.search.items.map(i => i.productName)).toEqual([
  190. 'Bonsai Tree',
  191. 'Camera Lens',
  192. 'Clacky Keyboard',
  193. 'Curvy Monitor',
  194. 'Gaming PC',
  195. 'Hard Drive',
  196. 'Instant Camera',
  197. 'Laptop',
  198. 'Orchid',
  199. 'SLR Camera',
  200. 'Spiky Cactus',
  201. 'Tripod',
  202. 'USB Cable',
  203. ]);
  204. }
  205. async function testMatchCollectionId(client: SimpleGraphQLClient) {
  206. const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  207. SEARCH_PRODUCTS_SHOP,
  208. {
  209. input: {
  210. collectionId: 'T_2',
  211. groupByProduct: true,
  212. },
  213. },
  214. );
  215. expect(result.search.items.map(i => i.productName)).toEqual([
  216. 'Spiky Cactus',
  217. 'Orchid',
  218. 'Bonsai Tree',
  219. ]);
  220. }
  221. async function testMatchCollectionSlug(client: SimpleGraphQLClient) {
  222. const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  223. SEARCH_PRODUCTS_SHOP,
  224. {
  225. input: {
  226. collectionSlug: 'plants',
  227. groupByProduct: true,
  228. },
  229. },
  230. );
  231. expect(result.search.items.map(i => i.productName)).toEqual([
  232. 'Spiky Cactus',
  233. 'Orchid',
  234. 'Bonsai Tree',
  235. ]);
  236. }
  237. async function testSinglePrices(client: SimpleGraphQLClient) {
  238. const result = await client.query<SearchGetPrices.Query, SearchGetPrices.Variables>(
  239. SEARCH_GET_PRICES,
  240. {
  241. input: {
  242. groupByProduct: false,
  243. take: 3,
  244. sort: {
  245. price: SortOrder.ASC,
  246. },
  247. },
  248. },
  249. );
  250. expect(result.search.items).toEqual([
  251. {
  252. price: { value: 799 },
  253. priceWithTax: { value: 959 },
  254. },
  255. {
  256. price: { value: 1498 },
  257. priceWithTax: { value: 1798 },
  258. },
  259. {
  260. price: { value: 1550 },
  261. priceWithTax: { value: 1860 },
  262. },
  263. ]);
  264. }
  265. async function testPriceRanges(client: SimpleGraphQLClient) {
  266. const result = await client.query<SearchGetPrices.Query, SearchGetPrices.Variables>(
  267. SEARCH_GET_PRICES,
  268. {
  269. input: {
  270. groupByProduct: true,
  271. take: 3,
  272. term: 'laptop',
  273. },
  274. },
  275. );
  276. expect(result.search.items).toEqual([
  277. {
  278. price: { min: 129900, max: 229900 },
  279. priceWithTax: { min: 155880, max: 275880 },
  280. },
  281. ]);
  282. }
  283. describe('shop api', () => {
  284. it('group by product', () => testGroupByProduct(shopClient));
  285. it('no grouping', () => testNoGrouping(shopClient));
  286. it('matches search term', () => testMatchSearchTerm(shopClient));
  287. it('matches by facetValueId with AND operator', () => testMatchFacetIdsAnd(shopClient));
  288. it('matches by facetValueId with OR operator', () => testMatchFacetIdsOr(shopClient));
  289. it('matches by collectionId', () => testMatchCollectionId(shopClient));
  290. it('matches by collectionSlug', () => testMatchCollectionSlug(shopClient));
  291. it('single prices', () => testSinglePrices(shopClient));
  292. it('price ranges', () => testPriceRanges(shopClient));
  293. it('returns correct facetValues when not grouped by product', async () => {
  294. const result = await shopClient.query<SearchFacetValues.Query, SearchFacetValues.Variables>(
  295. SEARCH_GET_FACET_VALUES,
  296. {
  297. input: {
  298. groupByProduct: false,
  299. },
  300. },
  301. );
  302. expect(result.search.facetValues).toEqual([
  303. { count: 21, facetValue: { id: 'T_1', name: 'electronics' } },
  304. { count: 17, facetValue: { id: 'T_2', name: 'computers' } },
  305. { count: 4, facetValue: { id: 'T_3', name: 'photo' } },
  306. { count: 10, facetValue: { id: 'T_4', name: 'sports equipment' } },
  307. { count: 3, facetValue: { id: 'T_5', name: 'home & garden' } },
  308. { count: 3, facetValue: { id: 'T_6', name: 'plants' } },
  309. ]);
  310. });
  311. it('returns correct facetValues when grouped by product', async () => {
  312. const result = await shopClient.query<SearchFacetValues.Query, SearchFacetValues.Variables>(
  313. SEARCH_GET_FACET_VALUES,
  314. {
  315. input: {
  316. groupByProduct: true,
  317. },
  318. },
  319. );
  320. expect(result.search.facetValues).toEqual([
  321. { count: 10, facetValue: { id: 'T_1', name: 'electronics' } },
  322. { count: 6, facetValue: { id: 'T_2', name: 'computers' } },
  323. { count: 4, facetValue: { id: 'T_3', name: 'photo' } },
  324. { count: 7, facetValue: { id: 'T_4', name: 'sports equipment' } },
  325. { count: 3, facetValue: { id: 'T_5', name: 'home & garden' } },
  326. { count: 3, facetValue: { id: 'T_6', name: 'plants' } },
  327. ]);
  328. });
  329. it('omits facetValues of private facets', async () => {
  330. const { createFacet } = await adminClient.query<CreateFacet.Mutation, CreateFacet.Variables>(
  331. CREATE_FACET,
  332. {
  333. input: {
  334. code: 'profit-margin',
  335. isPrivate: true,
  336. translations: [{ languageCode: LanguageCode.en, name: 'Profit Margin' }],
  337. values: [
  338. {
  339. code: 'massive',
  340. translations: [{ languageCode: LanguageCode.en, name: 'massive' }],
  341. },
  342. ],
  343. },
  344. },
  345. );
  346. await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
  347. input: {
  348. id: 'T_2',
  349. // T_1 & T_2 are the existing facetValues (electronics & photo)
  350. facetValueIds: ['T_1', 'T_2', createFacet.values[0].id],
  351. },
  352. });
  353. await awaitRunningJobs(adminClient);
  354. const result = await shopClient.query<SearchFacetValues.Query, SearchFacetValues.Variables>(
  355. SEARCH_GET_FACET_VALUES,
  356. {
  357. input: {
  358. groupByProduct: true,
  359. },
  360. },
  361. );
  362. expect(result.search.facetValues).toEqual([
  363. { count: 10, facetValue: { id: 'T_1', name: 'electronics' } },
  364. { count: 6, facetValue: { id: 'T_2', name: 'computers' } },
  365. { count: 4, facetValue: { id: 'T_3', name: 'photo' } },
  366. { count: 7, facetValue: { id: 'T_4', name: 'sports equipment' } },
  367. { count: 3, facetValue: { id: 'T_5', name: 'home & garden' } },
  368. { count: 3, facetValue: { id: 'T_6', name: 'plants' } },
  369. ]);
  370. });
  371. it('encodes the productId and productVariantId', async () => {
  372. const result = await shopClient.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  373. SEARCH_PRODUCTS_SHOP,
  374. {
  375. input: {
  376. groupByProduct: false,
  377. take: 1,
  378. },
  379. },
  380. );
  381. expect(pick(result.search.items[0], ['productId', 'productVariantId'])).toEqual({
  382. productId: 'T_1',
  383. productVariantId: 'T_1',
  384. });
  385. });
  386. it('omits results for disabled ProductVariants', async () => {
  387. await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
  388. UPDATE_PRODUCT_VARIANTS,
  389. {
  390. input: [{ id: 'T_3', enabled: false }],
  391. },
  392. );
  393. await awaitRunningJobs(adminClient);
  394. const result = await shopClient.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  395. SEARCH_PRODUCTS_SHOP,
  396. {
  397. input: {
  398. groupByProduct: false,
  399. take: 3,
  400. },
  401. },
  402. );
  403. expect(result.search.items.map(i => i.productVariantId)).toEqual(['T_1', 'T_2', 'T_4']);
  404. });
  405. it('encodes collectionIds', async () => {
  406. const result = await shopClient.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  407. SEARCH_PRODUCTS_SHOP,
  408. {
  409. input: {
  410. groupByProduct: false,
  411. term: 'cactus',
  412. take: 1,
  413. },
  414. },
  415. );
  416. expect(result.search.items[0].collectionIds).toEqual(['T_2']);
  417. });
  418. });
  419. describe('admin api', () => {
  420. it('group by product', () => testGroupByProduct(adminClient));
  421. it('no grouping', () => testNoGrouping(adminClient));
  422. it('matches search term', () => testMatchSearchTerm(adminClient));
  423. it('matches by facetValueId with AND operator', () => testMatchFacetIdsAnd(adminClient));
  424. it('matches by facetValueId with OR operator', () => testMatchFacetIdsOr(adminClient));
  425. it('matches by collectionId', () => testMatchCollectionId(adminClient));
  426. it('matches by collectionSlug', () => testMatchCollectionSlug(adminClient));
  427. it('single prices', () => testSinglePrices(adminClient));
  428. it('price ranges', () => testPriceRanges(adminClient));
  429. describe('updating the index', () => {
  430. it('updates index when ProductVariants are changed', async () => {
  431. await awaitRunningJobs(adminClient);
  432. const { search } = await doAdminSearchQuery({ term: 'drive', groupByProduct: false });
  433. expect(search.items.map(i => i.sku)).toEqual([
  434. 'IHD455T1',
  435. 'IHD455T2',
  436. 'IHD455T3',
  437. 'IHD455T4',
  438. 'IHD455T6',
  439. ]);
  440. await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
  441. UPDATE_PRODUCT_VARIANTS,
  442. {
  443. input: search.items.map(i => ({
  444. id: i.productVariantId,
  445. sku: i.sku + '_updated',
  446. })),
  447. },
  448. );
  449. await awaitRunningJobs(adminClient);
  450. const { search: search2 } = await doAdminSearchQuery({
  451. term: 'drive',
  452. groupByProduct: false,
  453. });
  454. expect(search2.items.map(i => i.sku)).toEqual([
  455. 'IHD455T1_updated',
  456. 'IHD455T2_updated',
  457. 'IHD455T3_updated',
  458. 'IHD455T4_updated',
  459. 'IHD455T6_updated',
  460. ]);
  461. });
  462. it('updates index when ProductVariants are deleted', async () => {
  463. await awaitRunningJobs(adminClient);
  464. const { search } = await doAdminSearchQuery({ term: 'drive', groupByProduct: false });
  465. await adminClient.query<DeleteProductVariant.Mutation, DeleteProductVariant.Variables>(
  466. DELETE_PRODUCT_VARIANT,
  467. {
  468. id: search.items[0].productVariantId,
  469. },
  470. );
  471. await awaitRunningJobs(adminClient);
  472. const { search: search2 } = await doAdminSearchQuery({
  473. term: 'drive',
  474. groupByProduct: false,
  475. });
  476. expect(search2.items.map(i => i.sku).sort()).toEqual([
  477. 'IHD455T2_updated',
  478. 'IHD455T3_updated',
  479. 'IHD455T4_updated',
  480. 'IHD455T6_updated',
  481. ]);
  482. });
  483. it('updates index when a Product is changed', async () => {
  484. await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
  485. input: {
  486. id: 'T_1',
  487. facetValueIds: [],
  488. },
  489. });
  490. await awaitRunningJobs(adminClient);
  491. const result = await doAdminSearchQuery({ facetValueIds: ['T_2'], groupByProduct: true });
  492. expect(result.search.items.map(i => i.productName).sort()).toEqual([
  493. 'Clacky Keyboard',
  494. 'Curvy Monitor',
  495. 'Gaming PC',
  496. 'Hard Drive',
  497. 'USB Cable',
  498. ]);
  499. });
  500. it('updates index when a Product is deleted', async () => {
  501. const { search } = await doAdminSearchQuery({ facetValueIds: ['T_2'], groupByProduct: true });
  502. expect(search.items.map(i => i.productId).sort()).toEqual([
  503. 'T_2',
  504. 'T_3',
  505. 'T_4',
  506. 'T_5',
  507. 'T_6',
  508. ]);
  509. await adminClient.query<DeleteProduct.Mutation, DeleteProduct.Variables>(DELETE_PRODUCT, {
  510. id: 'T_5',
  511. });
  512. await awaitRunningJobs(adminClient);
  513. const { search: search2 } = await doAdminSearchQuery({
  514. facetValueIds: ['T_2'],
  515. groupByProduct: true,
  516. });
  517. expect(search2.items.map(i => i.productId).sort()).toEqual(['T_2', 'T_3', 'T_4', 'T_6']);
  518. });
  519. it('updates index when a Collection is changed', async () => {
  520. await adminClient.query<UpdateCollection.Mutation, UpdateCollection.Variables>(
  521. UPDATE_COLLECTION,
  522. {
  523. input: {
  524. id: 'T_2',
  525. filters: [
  526. {
  527. code: facetValueCollectionFilter.code,
  528. arguments: [
  529. {
  530. name: 'facetValueIds',
  531. value: `["T_4"]`,
  532. },
  533. {
  534. name: 'containsAny',
  535. value: `false`,
  536. },
  537. ],
  538. },
  539. ],
  540. },
  541. },
  542. );
  543. await awaitRunningJobs(adminClient);
  544. // add an additional check for the collection filters to update
  545. await awaitRunningJobs(adminClient);
  546. const result1 = await doAdminSearchQuery({ collectionId: 'T_2', groupByProduct: true });
  547. expect(result1.search.items.map(i => i.productName)).toEqual([
  548. 'Road Bike',
  549. 'Skipping Rope',
  550. 'Boxing Gloves',
  551. 'Tent',
  552. 'Cruiser Skateboard',
  553. 'Football',
  554. 'Running Shoe',
  555. ]);
  556. const result2 = await doAdminSearchQuery({ collectionSlug: 'plants', groupByProduct: true });
  557. expect(result2.search.items.map(i => i.productName)).toEqual([
  558. 'Road Bike',
  559. 'Skipping Rope',
  560. 'Boxing Gloves',
  561. 'Tent',
  562. 'Cruiser Skateboard',
  563. 'Football',
  564. 'Running Shoe',
  565. ]);
  566. });
  567. it('updates index when a Collection created', async () => {
  568. const { createCollection } = await adminClient.query<
  569. CreateCollection.Mutation,
  570. CreateCollection.Variables
  571. >(CREATE_COLLECTION, {
  572. input: {
  573. translations: [
  574. {
  575. languageCode: LanguageCode.en,
  576. name: 'Photo',
  577. description: '',
  578. slug: 'photo',
  579. },
  580. ],
  581. filters: [
  582. {
  583. code: facetValueCollectionFilter.code,
  584. arguments: [
  585. {
  586. name: 'facetValueIds',
  587. value: `["T_3"]`,
  588. },
  589. {
  590. name: 'containsAny',
  591. value: `false`,
  592. },
  593. ],
  594. },
  595. ],
  596. },
  597. });
  598. await awaitRunningJobs(adminClient);
  599. // add an additional check for the collection filters to update
  600. await awaitRunningJobs(adminClient);
  601. const result = await doAdminSearchQuery({
  602. collectionId: createCollection.id,
  603. groupByProduct: true,
  604. });
  605. expect(result.search.items.map(i => i.productName)).toEqual([
  606. 'Instant Camera',
  607. 'Camera Lens',
  608. 'Tripod',
  609. 'SLR Camera',
  610. ]);
  611. });
  612. it('updates index when a taxRate is changed', async () => {
  613. await adminClient.query<UpdateTaxRate.Mutation, UpdateTaxRate.Variables>(UPDATE_TAX_RATE, {
  614. input: {
  615. // Default Channel's defaultTaxZone is Europe (id 2) and the id of the standard TaxRate
  616. // to Europe is 2.
  617. id: 'T_2',
  618. value: 50,
  619. },
  620. });
  621. await awaitRunningJobs(adminClient);
  622. const result = await adminClient.query<SearchGetPrices.Query, SearchGetPrices.Variables>(
  623. SEARCH_GET_PRICES,
  624. {
  625. input: {
  626. groupByProduct: true,
  627. term: 'laptop',
  628. } as SearchInput,
  629. },
  630. );
  631. expect(result.search.items).toEqual([
  632. {
  633. price: { min: 129900, max: 229900 },
  634. priceWithTax: { min: 194850, max: 344850 },
  635. },
  636. ]);
  637. });
  638. describe('asset changes', () => {
  639. function searchForLaptop() {
  640. return doAdminSearchQuery({
  641. term: 'laptop',
  642. groupByProduct: true,
  643. take: 1,
  644. sort: {
  645. name: SortOrder.ASC,
  646. },
  647. });
  648. }
  649. it('updates index when asset focalPoint is changed', async () => {
  650. const { search: search1 } = await searchForLaptop();
  651. expect(search1.items[0].productAsset!.id).toBe('T_1');
  652. expect(search1.items[0].productAsset!.focalPoint).toBeNull();
  653. await adminClient.query<UpdateAsset.Mutation, UpdateAsset.Variables>(UPDATE_ASSET, {
  654. input: {
  655. id: 'T_1',
  656. focalPoint: {
  657. x: 0.42,
  658. y: 0.42,
  659. },
  660. },
  661. });
  662. await awaitRunningJobs(adminClient);
  663. const { search: search2 } = await searchForLaptop();
  664. expect(search2.items[0].productAsset!.id).toBe('T_1');
  665. expect(search2.items[0].productAsset!.focalPoint).toEqual({ x: 0.42, y: 0.42 });
  666. });
  667. it('updates index when asset deleted', async () => {
  668. const { search: search1 } = await searchForLaptop();
  669. const assetId = search1.items[0].productAsset?.id;
  670. expect(assetId).toBeTruthy();
  671. await adminClient.query<DeleteAsset.Mutation, DeleteAsset.Variables>(DELETE_ASSET, {
  672. id: assetId!,
  673. force: true,
  674. });
  675. await awaitRunningJobs(adminClient);
  676. const { search: search2 } = await searchForLaptop();
  677. expect(search2.items[0].productAsset).toBeNull();
  678. });
  679. });
  680. it('does not include deleted ProductVariants in index', async () => {
  681. const { search: s1 } = await doAdminSearchQuery({
  682. term: 'hard drive',
  683. groupByProduct: false,
  684. });
  685. const variantToDelete = s1.items.find(i => i.sku === 'IHD455T2_updated')!;
  686. const { deleteProductVariant } = await adminClient.query<
  687. DeleteProductVariant.Mutation,
  688. DeleteProductVariant.Variables
  689. >(DELETE_PRODUCT_VARIANT, { id: variantToDelete.productVariantId });
  690. await awaitRunningJobs(adminClient);
  691. const { search } = await adminClient.query<SearchGetPrices.Query, SearchGetPrices.Variables>(
  692. SEARCH_GET_PRICES,
  693. { input: { term: 'hard drive', groupByProduct: true } },
  694. );
  695. expect(search.items[0].price).toEqual({
  696. min: 7896,
  697. max: 13435,
  698. });
  699. });
  700. it('returns disabled field when not grouped', async () => {
  701. const result = await doAdminSearchQuery({ groupByProduct: false, term: 'laptop' });
  702. expect(result.search.items.map(pick(['productVariantId', 'enabled']))).toEqual([
  703. { productVariantId: 'T_1', enabled: true },
  704. { productVariantId: 'T_2', enabled: true },
  705. { productVariantId: 'T_3', enabled: false },
  706. { productVariantId: 'T_4', enabled: true },
  707. ]);
  708. });
  709. it('when grouped, disabled is false if at least one variant is enabled', async () => {
  710. await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
  711. UPDATE_PRODUCT_VARIANTS,
  712. {
  713. input: [
  714. { id: 'T_1', enabled: false },
  715. { id: 'T_2', enabled: false },
  716. ],
  717. },
  718. );
  719. await awaitRunningJobs(adminClient);
  720. const result = await doAdminSearchQuery({ groupByProduct: true, term: 'laptop' });
  721. expect(result.search.items.map(pick(['productId', 'enabled']))).toEqual([
  722. { productId: 'T_1', enabled: true },
  723. ]);
  724. });
  725. it('when grouped, disabled is true if all variants are disabled', async () => {
  726. await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
  727. UPDATE_PRODUCT_VARIANTS,
  728. {
  729. input: [{ id: 'T_4', enabled: false }],
  730. },
  731. );
  732. await awaitRunningJobs(adminClient);
  733. const result = await doAdminSearchQuery({ groupByProduct: true, take: 3, term: 'laptop' });
  734. expect(result.search.items.map(pick(['productId', 'enabled']))).toEqual([
  735. { productId: 'T_1', enabled: false },
  736. ]);
  737. });
  738. it('when grouped, disabled is true product is disabled', async () => {
  739. await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
  740. input: {
  741. id: 'T_3',
  742. enabled: false,
  743. },
  744. });
  745. await awaitRunningJobs(adminClient);
  746. const result = await doAdminSearchQuery({ groupByProduct: true, term: 'gaming' });
  747. expect(result.search.items.map(pick(['productId', 'enabled']))).toEqual([
  748. { productId: 'T_3', enabled: false },
  749. ]);
  750. });
  751. // https://github.com/vendure-ecommerce/vendure/issues/295
  752. it('enabled status survives reindex', async () => {
  753. await adminClient.query<Reindex.Mutation>(REINDEX);
  754. await awaitRunningJobs(adminClient);
  755. const result = await doAdminSearchQuery({ groupByProduct: true, take: 3 });
  756. expect(result.search.items.map(pick(['productId', 'enabled']))).toEqual([
  757. { productId: 'T_1', enabled: false },
  758. { productId: 'T_2', enabled: true },
  759. { productId: 'T_3', enabled: false },
  760. ]);
  761. });
  762. });
  763. describe('channel handling', () => {
  764. const SECOND_CHANNEL_TOKEN = 'second-channel-token';
  765. let secondChannel: CreateChannel.CreateChannel;
  766. beforeAll(async () => {
  767. const { createChannel } = await adminClient.query<
  768. CreateChannel.Mutation,
  769. CreateChannel.Variables
  770. >(CREATE_CHANNEL, {
  771. input: {
  772. code: 'second-channel',
  773. token: SECOND_CHANNEL_TOKEN,
  774. defaultLanguageCode: LanguageCode.en,
  775. currencyCode: CurrencyCode.GBP,
  776. pricesIncludeTax: true,
  777. defaultTaxZoneId: 'T_2',
  778. defaultShippingZoneId: 'T_1',
  779. },
  780. });
  781. secondChannel = createChannel;
  782. });
  783. it('adding product to channel', async () => {
  784. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  785. await adminClient.query<AssignProductsToChannel.Mutation, AssignProductsToChannel.Variables>(
  786. ASSIGN_PRODUCT_TO_CHANNEL,
  787. {
  788. input: { channelId: secondChannel.id, productIds: ['T_1', 'T_2'] },
  789. },
  790. );
  791. await awaitRunningJobs(adminClient);
  792. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  793. const { search } = await doAdminSearchQuery({ groupByProduct: true });
  794. expect(search.items.map(i => i.productId).sort()).toEqual(['T_1', 'T_2']);
  795. });
  796. it('removing product from channel', async () => {
  797. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  798. const { removeProductsFromChannel } = await adminClient.query<
  799. RemoveProductsFromChannel.Mutation,
  800. RemoveProductsFromChannel.Variables
  801. >(REMOVE_PRODUCT_FROM_CHANNEL, {
  802. input: {
  803. productIds: ['T_2'],
  804. channelId: secondChannel.id,
  805. },
  806. });
  807. await awaitRunningJobs(adminClient);
  808. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  809. const { search } = await doAdminSearchQuery({ groupByProduct: true });
  810. expect(search.items.map(i => i.productId)).toEqual(['T_1']);
  811. });
  812. it('reindexes in channel', async () => {
  813. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  814. const { reindex } = await adminClient.query<Reindex.Mutation>(REINDEX);
  815. await awaitRunningJobs(adminClient);
  816. const { job } = await adminClient.query<GetJobInfo.Query, GetJobInfo.Variables>(
  817. GET_JOB_INFO,
  818. { id: reindex.id },
  819. );
  820. expect(job!.state).toBe(JobState.COMPLETED);
  821. const { search } = await doAdminSearchQuery({ groupByProduct: true });
  822. expect(search.items.map(i => i.productId).sort()).toEqual(['T_1']);
  823. });
  824. });
  825. });
  826. });
  827. export const SEARCH_PRODUCTS = gql`
  828. query SearchProductsAdmin($input: SearchInput!) {
  829. search(input: $input) {
  830. totalItems
  831. items {
  832. enabled
  833. productId
  834. productName
  835. productAsset {
  836. id
  837. preview
  838. focalPoint {
  839. x
  840. y
  841. }
  842. }
  843. productPreview
  844. productVariantId
  845. productVariantName
  846. productVariantAsset {
  847. id
  848. preview
  849. focalPoint {
  850. x
  851. y
  852. }
  853. }
  854. productVariantPreview
  855. sku
  856. }
  857. }
  858. }
  859. `;
  860. export const SEARCH_GET_FACET_VALUES = gql`
  861. query SearchFacetValues($input: SearchInput!) {
  862. search(input: $input) {
  863. totalItems
  864. facetValues {
  865. count
  866. facetValue {
  867. id
  868. name
  869. }
  870. }
  871. }
  872. }
  873. `;
  874. export const SEARCH_GET_PRICES = gql`
  875. query SearchGetPrices($input: SearchInput!) {
  876. search(input: $input) {
  877. items {
  878. price {
  879. ... on PriceRange {
  880. min
  881. max
  882. }
  883. ... on SinglePrice {
  884. value
  885. }
  886. }
  887. priceWithTax {
  888. ... on PriceRange {
  889. min
  890. max
  891. }
  892. ... on SinglePrice {
  893. value
  894. }
  895. }
  896. }
  897. }
  898. }
  899. `;
  900. const REINDEX = gql`
  901. mutation Reindex {
  902. reindex {
  903. id
  904. queueName
  905. state
  906. progress
  907. duration
  908. result
  909. }
  910. }
  911. `;
  912. const GET_JOB_INFO = gql`
  913. query GetJobInfo($id: ID!) {
  914. job(jobId: $id) {
  915. id
  916. queueName
  917. state
  918. progress
  919. duration
  920. result
  921. }
  922. }
  923. `;