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

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