elasticsearch-plugin.e2e-spec.ts 35 KB

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