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

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