default-search-plugin.e2e-spec.ts 34 KB

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