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

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577
  1. /* tslint:disable:no-non-null-assertion */
  2. import { pick } from '@vendure/common/lib/pick';
  3. import {
  4. DefaultJobQueuePlugin,
  5. DefaultLogger,
  6. DefaultSearchPlugin,
  7. facetValueCollectionFilter,
  8. mergeConfig,
  9. } from '@vendure/core';
  10. import { createTestEnvironment, E2E_DEFAULT_CHANNEL_TOKEN, SimpleGraphQLClient } from '@vendure/testing';
  11. import gql from 'graphql-tag';
  12. import path from 'path';
  13. import { initialData } from '../../../e2e-common/e2e-initial-data';
  14. import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
  15. import {
  16. AssignProductsToChannel,
  17. AssignProductVariantsToChannel,
  18. ChannelFragment,
  19. CreateChannel,
  20. CreateCollection,
  21. CreateFacet,
  22. CreateProduct,
  23. CreateProductVariants,
  24. CurrencyCode,
  25. DeleteAsset,
  26. DeleteProduct,
  27. DeleteProductVariant,
  28. LanguageCode,
  29. Reindex,
  30. RemoveProductsFromChannel,
  31. RemoveProductVariantsFromChannel,
  32. SearchCollections,
  33. SearchFacetValues,
  34. SearchGetAssets,
  35. SearchGetPrices,
  36. SearchInput,
  37. SearchResultSortParameter,
  38. SortOrder,
  39. UpdateAsset,
  40. UpdateCollection,
  41. UpdateProduct,
  42. UpdateProductVariants,
  43. UpdateTaxRate,
  44. } from './graphql/generated-e2e-admin-types';
  45. import { LogicalOperator, SearchProductsShop } from './graphql/generated-e2e-shop-types';
  46. import {
  47. ASSIGN_PRODUCTVARIANT_TO_CHANNEL,
  48. ASSIGN_PRODUCT_TO_CHANNEL,
  49. CREATE_CHANNEL,
  50. CREATE_COLLECTION,
  51. CREATE_FACET,
  52. CREATE_PRODUCT,
  53. CREATE_PRODUCT_VARIANTS,
  54. DELETE_ASSET,
  55. DELETE_PRODUCT,
  56. DELETE_PRODUCT_VARIANT,
  57. REMOVE_PRODUCTVARIANT_FROM_CHANNEL,
  58. REMOVE_PRODUCT_FROM_CHANNEL,
  59. UPDATE_ASSET,
  60. UPDATE_COLLECTION,
  61. UPDATE_PRODUCT,
  62. UPDATE_PRODUCT_VARIANTS,
  63. UPDATE_TAX_RATE,
  64. } from './graphql/shared-definitions';
  65. import { SEARCH_PRODUCTS_SHOP } from './graphql/shop-definitions';
  66. import { awaitRunningJobs } from './utils/await-running-jobs';
  67. // Some of these tests have many steps and can timeout
  68. // on the default of 5s.
  69. jest.setTimeout(10000);
  70. describe('Default search plugin', () => {
  71. const { server, adminClient, shopClient } = createTestEnvironment(
  72. mergeConfig(testConfig, {
  73. logger: new DefaultLogger(),
  74. plugins: [DefaultSearchPlugin, DefaultJobQueuePlugin],
  75. }),
  76. );
  77. beforeAll(async () => {
  78. await server.init({
  79. initialData,
  80. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
  81. customerCount: 1,
  82. });
  83. await adminClient.asSuperAdmin();
  84. }, TEST_SETUP_TIMEOUT_MS);
  85. afterAll(async () => {
  86. await awaitRunningJobs(adminClient);
  87. await server.destroy();
  88. });
  89. function doAdminSearchQuery(input: SearchInput) {
  90. return adminClient.query<SearchProductsShop.Query, SearchProductsShop.Variables>(SEARCH_PRODUCTS, {
  91. input,
  92. });
  93. }
  94. async function testGroupByProduct(client: SimpleGraphQLClient) {
  95. const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  96. SEARCH_PRODUCTS_SHOP,
  97. {
  98. input: {
  99. groupByProduct: true,
  100. },
  101. },
  102. );
  103. expect(result.search.totalItems).toBe(20);
  104. }
  105. async function testNoGrouping(client: SimpleGraphQLClient) {
  106. const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  107. SEARCH_PRODUCTS_SHOP,
  108. {
  109. input: {
  110. groupByProduct: false,
  111. },
  112. },
  113. );
  114. expect(result.search.totalItems).toBe(34);
  115. }
  116. async function testSortingWithGrouping(
  117. client: SimpleGraphQLClient,
  118. sortBy: keyof SearchResultSortParameter,
  119. ) {
  120. const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  121. SEARCH_PRODUCTS_SHOP,
  122. {
  123. input: {
  124. groupByProduct: true,
  125. sort: {
  126. [sortBy]: SortOrder.ASC,
  127. },
  128. take: 3,
  129. },
  130. },
  131. );
  132. const expected =
  133. sortBy === 'name'
  134. ? ['Bonsai Tree', 'Boxing Gloves', 'Camera Lens']
  135. : ['Skipping Rope', 'Tripod', 'Spiky Cactus'];
  136. expect(result.search.items.map(i => i.productName)).toEqual(expected);
  137. }
  138. async function testSortingNoGrouping(
  139. client: SimpleGraphQLClient,
  140. sortBy: keyof SearchResultSortParameter,
  141. ) {
  142. const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  143. SEARCH_PRODUCTS_SHOP,
  144. {
  145. input: {
  146. groupByProduct: false,
  147. sort: {
  148. [sortBy]: SortOrder.DESC,
  149. },
  150. take: 3,
  151. },
  152. },
  153. );
  154. const expected =
  155. sortBy === 'name'
  156. ? ['USB Cable', 'Tripod', 'Tent']
  157. : ['Road Bike', 'Laptop 15 inch 16GB', 'Laptop 13 inch 16GB'];
  158. expect(result.search.items.map(i => i.productVariantName)).toEqual(expected);
  159. }
  160. async function testMatchSearchTerm(client: SimpleGraphQLClient) {
  161. const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  162. SEARCH_PRODUCTS_SHOP,
  163. {
  164. input: {
  165. term: 'camera',
  166. groupByProduct: true,
  167. sort: {
  168. name: SortOrder.ASC,
  169. },
  170. },
  171. },
  172. );
  173. expect(result.search.items.map(i => i.productName)).toEqual([
  174. 'Camera Lens',
  175. 'Instant Camera',
  176. 'Slr Camera',
  177. ]);
  178. }
  179. async function testMatchFacetIdsAnd(client: SimpleGraphQLClient) {
  180. const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  181. SEARCH_PRODUCTS_SHOP,
  182. {
  183. input: {
  184. facetValueIds: ['T_1', 'T_2'],
  185. facetValueOperator: LogicalOperator.AND,
  186. groupByProduct: true,
  187. },
  188. },
  189. );
  190. expect(result.search.items.map(i => i.productName)).toEqual([
  191. 'Laptop',
  192. 'Curvy Monitor',
  193. 'Gaming PC',
  194. 'Hard Drive',
  195. 'Clacky Keyboard',
  196. 'USB Cable',
  197. ]);
  198. }
  199. async function testMatchFacetIdsOr(client: SimpleGraphQLClient) {
  200. const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  201. SEARCH_PRODUCTS_SHOP,
  202. {
  203. input: {
  204. facetValueIds: ['T_1', 'T_5'],
  205. facetValueOperator: LogicalOperator.OR,
  206. groupByProduct: true,
  207. },
  208. },
  209. );
  210. expect(result.search.items.map(i => i.productName)).toEqual([
  211. 'Laptop',
  212. 'Curvy Monitor',
  213. 'Gaming PC',
  214. 'Hard Drive',
  215. 'Clacky Keyboard',
  216. 'USB Cable',
  217. 'Instant Camera',
  218. 'Camera Lens',
  219. 'Tripod',
  220. 'Slr Camera',
  221. 'Spiky Cactus',
  222. 'Orchid',
  223. 'Bonsai Tree',
  224. ]);
  225. }
  226. async function testMatchFacetValueFiltersAnd(client: SimpleGraphQLClient) {
  227. const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  228. SEARCH_PRODUCTS_SHOP,
  229. {
  230. input: {
  231. groupByProduct: true,
  232. facetValueFilters: [{ and: 'T_1' }, { and: 'T_2' }],
  233. },
  234. },
  235. );
  236. expect(result.search.items.map(i => i.productName)).toEqual([
  237. 'Laptop',
  238. 'Curvy Monitor',
  239. 'Gaming PC',
  240. 'Hard Drive',
  241. 'Clacky Keyboard',
  242. 'USB Cable',
  243. ]);
  244. }
  245. async function testMatchFacetValueFiltersOr(client: SimpleGraphQLClient) {
  246. const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  247. SEARCH_PRODUCTS_SHOP,
  248. {
  249. input: {
  250. groupByProduct: true,
  251. facetValueFilters: [{ or: ['T_1', 'T_5'] }],
  252. },
  253. },
  254. );
  255. expect(result.search.items.map(i => i.productName)).toEqual([
  256. 'Laptop',
  257. 'Curvy Monitor',
  258. 'Gaming PC',
  259. 'Hard Drive',
  260. 'Clacky Keyboard',
  261. 'USB Cable',
  262. 'Instant Camera',
  263. 'Camera Lens',
  264. 'Tripod',
  265. 'Slr Camera',
  266. 'Spiky Cactus',
  267. 'Orchid',
  268. 'Bonsai Tree',
  269. ]);
  270. }
  271. async function testMatchFacetValueFiltersOrWithAnd(client: SimpleGraphQLClient) {
  272. const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  273. SEARCH_PRODUCTS_SHOP,
  274. {
  275. input: {
  276. groupByProduct: true,
  277. facetValueFilters: [{ and: 'T_1' }, { or: ['T_2', 'T_3'] }],
  278. },
  279. },
  280. );
  281. expect(result.search.items.map(i => i.productName)).toEqual([
  282. 'Laptop',
  283. 'Curvy Monitor',
  284. 'Gaming PC',
  285. 'Hard Drive',
  286. 'Clacky Keyboard',
  287. 'USB Cable',
  288. 'Instant Camera',
  289. 'Camera Lens',
  290. 'Tripod',
  291. 'Slr Camera',
  292. ]);
  293. }
  294. async function testMatchFacetValueFiltersWithFacetIdsOr(client: SimpleGraphQLClient) {
  295. const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  296. SEARCH_PRODUCTS_SHOP,
  297. {
  298. input: {
  299. facetValueIds: ['T_2', 'T_3'],
  300. facetValueOperator: LogicalOperator.OR,
  301. facetValueFilters: [{ and: 'T_1' }],
  302. groupByProduct: true,
  303. },
  304. },
  305. );
  306. expect(result.search.items.map(i => i.productName)).toEqual([
  307. 'Laptop',
  308. 'Curvy Monitor',
  309. 'Gaming PC',
  310. 'Hard Drive',
  311. 'Clacky Keyboard',
  312. 'USB Cable',
  313. 'Instant Camera',
  314. 'Camera Lens',
  315. 'Tripod',
  316. 'Slr Camera',
  317. ]);
  318. }
  319. async function testMatchFacetValueFiltersWithFacetIdsAnd(client: SimpleGraphQLClient) {
  320. const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  321. SEARCH_PRODUCTS_SHOP,
  322. {
  323. input: {
  324. facetValueIds: ['T_1'],
  325. facetValueFilters: [{ and: 'T_3' }],
  326. facetValueOperator: LogicalOperator.AND,
  327. groupByProduct: true,
  328. },
  329. },
  330. );
  331. expect(result.search.items.map(i => i.productName)).toEqual([
  332. 'Instant Camera',
  333. 'Camera Lens',
  334. 'Tripod',
  335. 'Slr Camera',
  336. ]);
  337. }
  338. async function testMatchCollectionId(client: SimpleGraphQLClient) {
  339. const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  340. SEARCH_PRODUCTS_SHOP,
  341. {
  342. input: {
  343. collectionId: 'T_2',
  344. groupByProduct: true,
  345. },
  346. },
  347. );
  348. expect(result.search.items.map(i => i.productName)).toEqual([
  349. 'Spiky Cactus',
  350. 'Orchid',
  351. 'Bonsai Tree',
  352. ]);
  353. }
  354. async function testMatchCollectionSlug(client: SimpleGraphQLClient) {
  355. const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  356. SEARCH_PRODUCTS_SHOP,
  357. {
  358. input: {
  359. collectionSlug: 'plants',
  360. groupByProduct: true,
  361. },
  362. },
  363. );
  364. expect(result.search.items.map(i => i.productName)).toEqual([
  365. 'Spiky Cactus',
  366. 'Orchid',
  367. 'Bonsai Tree',
  368. ]);
  369. }
  370. async function testSinglePrices(client: SimpleGraphQLClient) {
  371. const result = await client.query<SearchGetPrices.Query, SearchGetPrices.Variables>(
  372. SEARCH_GET_PRICES,
  373. {
  374. input: {
  375. groupByProduct: false,
  376. take: 3,
  377. } as SearchInput,
  378. },
  379. );
  380. expect(result.search.items).toEqual([
  381. {
  382. price: { value: 129900 },
  383. priceWithTax: { value: 155880 },
  384. },
  385. {
  386. price: { value: 139900 },
  387. priceWithTax: { value: 167880 },
  388. },
  389. {
  390. price: { value: 219900 },
  391. priceWithTax: { value: 263880 },
  392. },
  393. ]);
  394. }
  395. async function testPriceRanges(client: SimpleGraphQLClient) {
  396. const result = await client.query<SearchGetPrices.Query, SearchGetPrices.Variables>(
  397. SEARCH_GET_PRICES,
  398. {
  399. input: {
  400. groupByProduct: true,
  401. take: 3,
  402. } as SearchInput,
  403. },
  404. );
  405. expect(result.search.items).toEqual([
  406. {
  407. price: { min: 129900, max: 229900 },
  408. priceWithTax: { min: 155880, max: 275880 },
  409. },
  410. {
  411. price: { min: 14374, max: 16994 },
  412. priceWithTax: { min: 17249, max: 20393 },
  413. },
  414. {
  415. price: { min: 93120, max: 109995 },
  416. priceWithTax: { min: 111744, max: 131994 },
  417. },
  418. ]);
  419. }
  420. describe('shop api', () => {
  421. it('group by product', () => testGroupByProduct(shopClient));
  422. it('no grouping', () => testNoGrouping(shopClient));
  423. it('matches search term', () => testMatchSearchTerm(shopClient));
  424. it('matches by facetId with AND operator', () => testMatchFacetIdsAnd(shopClient));
  425. it('matches by facetId with OR operator', () => testMatchFacetIdsOr(shopClient));
  426. it('matches by FacetValueFilters AND', () => testMatchFacetValueFiltersAnd(shopClient));
  427. it('matches by FacetValueFilters OR', () => testMatchFacetValueFiltersOr(shopClient));
  428. it('matches by FacetValueFilters OR and AND', () => testMatchFacetValueFiltersOrWithAnd(shopClient));
  429. it('matches by FacetValueFilters with facetId OR operator', () =>
  430. testMatchFacetValueFiltersWithFacetIdsOr(shopClient));
  431. it('matches by FacetValueFilters with facetId AND operator', () =>
  432. testMatchFacetValueFiltersWithFacetIdsAnd(shopClient));
  433. it('matches by collectionId', () => testMatchCollectionId(shopClient));
  434. it('matches by collectionSlug', () => testMatchCollectionSlug(shopClient));
  435. it('single prices', () => testSinglePrices(shopClient));
  436. it('price ranges', () => testPriceRanges(shopClient));
  437. it('returns correct facetValues when not grouped by product', async () => {
  438. const result = await shopClient.query<SearchFacetValues.Query, SearchFacetValues.Variables>(
  439. SEARCH_GET_FACET_VALUES,
  440. {
  441. input: {
  442. groupByProduct: false,
  443. },
  444. },
  445. );
  446. expect(result.search.facetValues).toEqual([
  447. { count: 21, facetValue: { id: 'T_1', name: 'electronics' } },
  448. { count: 17, facetValue: { id: 'T_2', name: 'computers' } },
  449. { count: 4, facetValue: { id: 'T_3', name: 'photo' } },
  450. { count: 10, facetValue: { id: 'T_4', name: 'sports equipment' } },
  451. { count: 3, facetValue: { id: 'T_5', name: 'home & garden' } },
  452. { count: 3, facetValue: { id: 'T_6', name: 'plants' } },
  453. ]);
  454. });
  455. it('returns correct facetValues when grouped by product', async () => {
  456. const result = await shopClient.query<SearchFacetValues.Query, SearchFacetValues.Variables>(
  457. SEARCH_GET_FACET_VALUES,
  458. {
  459. input: {
  460. groupByProduct: true,
  461. },
  462. },
  463. );
  464. expect(result.search.facetValues).toEqual([
  465. { count: 10, facetValue: { id: 'T_1', name: 'electronics' } },
  466. { count: 6, facetValue: { id: 'T_2', name: 'computers' } },
  467. { count: 4, facetValue: { id: 'T_3', name: 'photo' } },
  468. { count: 7, facetValue: { id: 'T_4', name: 'sports equipment' } },
  469. { count: 3, facetValue: { id: 'T_5', name: 'home & garden' } },
  470. { count: 3, facetValue: { id: 'T_6', name: 'plants' } },
  471. ]);
  472. });
  473. it('omits facetValues of private facets', async () => {
  474. const { createFacet } = await adminClient.query<CreateFacet.Mutation, CreateFacet.Variables>(
  475. CREATE_FACET,
  476. {
  477. input: {
  478. code: 'profit-margin',
  479. isPrivate: true,
  480. translations: [{ languageCode: LanguageCode.en, name: 'Profit Margin' }],
  481. values: [
  482. {
  483. code: 'massive',
  484. translations: [{ languageCode: LanguageCode.en, name: 'massive' }],
  485. },
  486. ],
  487. },
  488. },
  489. );
  490. await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
  491. input: {
  492. id: 'T_2',
  493. // T_1 & T_2 are the existing facetValues (electronics & photo)
  494. facetValueIds: ['T_1', 'T_2', createFacet.values[0].id],
  495. },
  496. });
  497. await awaitRunningJobs(adminClient);
  498. const result = await shopClient.query<SearchFacetValues.Query, SearchFacetValues.Variables>(
  499. SEARCH_GET_FACET_VALUES,
  500. {
  501. input: {
  502. groupByProduct: true,
  503. },
  504. },
  505. );
  506. expect(result.search.facetValues).toEqual([
  507. { count: 10, facetValue: { id: 'T_1', name: 'electronics' } },
  508. { count: 6, facetValue: { id: 'T_2', name: 'computers' } },
  509. { count: 4, facetValue: { id: 'T_3', name: 'photo' } },
  510. { count: 7, facetValue: { id: 'T_4', name: 'sports equipment' } },
  511. { count: 3, facetValue: { id: 'T_5', name: 'home & garden' } },
  512. { count: 3, facetValue: { id: 'T_6', name: 'plants' } },
  513. ]);
  514. });
  515. it('returns correct collections when not grouped by product', async () => {
  516. const result = await shopClient.query<SearchCollections.Query, SearchCollections.Variables>(
  517. SEARCH_GET_COLLECTIONS,
  518. {
  519. input: {
  520. groupByProduct: false,
  521. },
  522. },
  523. );
  524. expect(result.search.collections).toEqual([
  525. { collection: { id: 'T_2', name: 'Plants' }, count: 3 },
  526. ]);
  527. });
  528. it('returns correct collections when grouped by product', async () => {
  529. const result = await shopClient.query<SearchCollections.Query, SearchCollections.Variables>(
  530. SEARCH_GET_COLLECTIONS,
  531. {
  532. input: {
  533. groupByProduct: true,
  534. },
  535. },
  536. );
  537. expect(result.search.collections).toEqual([
  538. { collection: { id: 'T_2', name: 'Plants' }, count: 3 },
  539. ]);
  540. });
  541. it('encodes the productId and productVariantId', async () => {
  542. const result = await shopClient.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  543. SEARCH_PRODUCTS_SHOP,
  544. {
  545. input: {
  546. groupByProduct: false,
  547. take: 1,
  548. },
  549. },
  550. );
  551. expect(pick(result.search.items[0], ['productId', 'productVariantId'])).toEqual({
  552. productId: 'T_1',
  553. productVariantId: 'T_1',
  554. });
  555. });
  556. it('sort name with grouping', () => testSortingWithGrouping(shopClient, 'name'));
  557. it('sort price with grouping', () => testSortingWithGrouping(shopClient, 'price'));
  558. it('sort name without grouping', () => testSortingNoGrouping(shopClient, 'name'));
  559. it('sort price without grouping', () => testSortingNoGrouping(shopClient, 'price'));
  560. it('omits results for disabled ProductVariants', async () => {
  561. await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
  562. UPDATE_PRODUCT_VARIANTS,
  563. {
  564. input: [{ id: 'T_3', enabled: false }],
  565. },
  566. );
  567. await awaitRunningJobs(adminClient);
  568. const result = await shopClient.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  569. SEARCH_PRODUCTS_SHOP,
  570. {
  571. input: {
  572. groupByProduct: false,
  573. take: 3,
  574. },
  575. },
  576. );
  577. expect(result.search.items.map(i => i.productVariantId)).toEqual(['T_1', 'T_2', 'T_4']);
  578. });
  579. it('encodes collectionIds', async () => {
  580. const result = await shopClient.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  581. SEARCH_PRODUCTS_SHOP,
  582. {
  583. input: {
  584. groupByProduct: false,
  585. term: 'cactus',
  586. take: 1,
  587. },
  588. },
  589. );
  590. expect(result.search.items[0].collectionIds).toEqual(['T_2']);
  591. });
  592. });
  593. describe('admin api', () => {
  594. it('group by product', () => testGroupByProduct(adminClient));
  595. it('no grouping', () => testNoGrouping(adminClient));
  596. it('matches search term', () => testMatchSearchTerm(adminClient));
  597. it('matches by facetId with AND operator', () => testMatchFacetIdsAnd(adminClient));
  598. it('matches by facetId with OR operator', () => testMatchFacetIdsOr(adminClient));
  599. it('matches by FacetValueFilters AND', () => testMatchFacetValueFiltersAnd(shopClient));
  600. it('matches by FacetValueFilters OR', () => testMatchFacetValueFiltersOr(shopClient));
  601. it('matches by FacetValueFilters OR and AND', () => testMatchFacetValueFiltersOrWithAnd(shopClient));
  602. it('matches by FacetValueFilters with facetId OR operator', () =>
  603. testMatchFacetValueFiltersWithFacetIdsOr(shopClient));
  604. it('matches by FacetValueFilters with facetId AND operator', () =>
  605. testMatchFacetValueFiltersWithFacetIdsAnd(shopClient));
  606. it('matches by collectionId', () => testMatchCollectionId(adminClient));
  607. it('matches by collectionSlug', () => testMatchCollectionSlug(adminClient));
  608. it('single prices', () => testSinglePrices(adminClient));
  609. it('price ranges', () => testPriceRanges(adminClient));
  610. it('sort name with grouping', () => testSortingWithGrouping(adminClient, 'name'));
  611. it('sort price with grouping', () => testSortingWithGrouping(adminClient, 'price'));
  612. it('sort name without grouping', () => testSortingNoGrouping(adminClient, 'name'));
  613. it('sort price without grouping', () => testSortingNoGrouping(adminClient, 'price'));
  614. describe('updating the index', () => {
  615. it('updates index when ProductVariants are changed', async () => {
  616. await awaitRunningJobs(adminClient);
  617. const { search } = await doAdminSearchQuery({ term: 'drive', groupByProduct: false });
  618. expect(search.items.map(i => i.sku)).toEqual([
  619. 'IHD455T1',
  620. 'IHD455T2',
  621. 'IHD455T3',
  622. 'IHD455T4',
  623. 'IHD455T6',
  624. ]);
  625. await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
  626. UPDATE_PRODUCT_VARIANTS,
  627. {
  628. input: search.items.map(i => ({
  629. id: i.productVariantId,
  630. sku: i.sku + '_updated',
  631. })),
  632. },
  633. );
  634. await awaitRunningJobs(adminClient);
  635. const { search: search2 } = await doAdminSearchQuery({
  636. term: 'drive',
  637. groupByProduct: false,
  638. });
  639. expect(search2.items.map(i => i.sku)).toEqual([
  640. 'IHD455T1_updated',
  641. 'IHD455T2_updated',
  642. 'IHD455T3_updated',
  643. 'IHD455T4_updated',
  644. 'IHD455T6_updated',
  645. ]);
  646. });
  647. it('updates index when ProductVariants are deleted', async () => {
  648. await awaitRunningJobs(adminClient);
  649. const { search } = await doAdminSearchQuery({ term: 'drive', groupByProduct: false });
  650. await adminClient.query<DeleteProductVariant.Mutation, DeleteProductVariant.Variables>(
  651. DELETE_PRODUCT_VARIANT,
  652. {
  653. id: search.items[0].productVariantId,
  654. },
  655. );
  656. await awaitRunningJobs(adminClient);
  657. const { search: search2 } = await doAdminSearchQuery({
  658. term: 'drive',
  659. groupByProduct: false,
  660. });
  661. expect(search2.items.map(i => i.sku)).toEqual([
  662. 'IHD455T2_updated',
  663. 'IHD455T3_updated',
  664. 'IHD455T4_updated',
  665. 'IHD455T6_updated',
  666. ]);
  667. });
  668. it('updates index when a Product is changed', async () => {
  669. await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
  670. input: {
  671. id: 'T_1',
  672. facetValueIds: [],
  673. },
  674. });
  675. await awaitRunningJobs(adminClient);
  676. const result = await doAdminSearchQuery({ facetValueIds: ['T_2'], groupByProduct: true });
  677. expect(result.search.items.map(i => i.productName)).toEqual([
  678. 'Curvy Monitor',
  679. 'Gaming PC',
  680. 'Hard Drive',
  681. 'Clacky Keyboard',
  682. 'USB Cable',
  683. ]);
  684. });
  685. it('updates index when a Product is deleted', async () => {
  686. const { search } = await doAdminSearchQuery({ facetValueIds: ['T_2'], groupByProduct: true });
  687. expect(search.items.map(i => i.productId)).toEqual(['T_2', 'T_3', 'T_4', 'T_5', 'T_6']);
  688. await adminClient.query<DeleteProduct.Mutation, DeleteProduct.Variables>(DELETE_PRODUCT, {
  689. id: 'T_5',
  690. });
  691. await awaitRunningJobs(adminClient);
  692. const { search: search2 } = await doAdminSearchQuery({
  693. facetValueIds: ['T_2'],
  694. groupByProduct: true,
  695. });
  696. expect(search2.items.map(i => i.productId)).toEqual(['T_2', 'T_3', 'T_4', 'T_6']);
  697. });
  698. it('updates index when a Collection is changed', async () => {
  699. await adminClient.query<UpdateCollection.Mutation, UpdateCollection.Variables>(
  700. UPDATE_COLLECTION,
  701. {
  702. input: {
  703. id: 'T_2',
  704. filters: [
  705. {
  706. code: facetValueCollectionFilter.code,
  707. arguments: [
  708. {
  709. name: 'facetValueIds',
  710. value: `["T_4"]`,
  711. },
  712. {
  713. name: 'containsAny',
  714. value: `false`,
  715. },
  716. ],
  717. },
  718. ],
  719. },
  720. },
  721. );
  722. await awaitRunningJobs(adminClient);
  723. // add an additional check for the collection filters to update
  724. await awaitRunningJobs(adminClient);
  725. const result1 = await doAdminSearchQuery({ collectionId: 'T_2', groupByProduct: true });
  726. expect(result1.search.items.map(i => i.productName)).toEqual([
  727. 'Road Bike',
  728. 'Skipping Rope',
  729. 'Boxing Gloves',
  730. 'Tent',
  731. 'Cruiser Skateboard',
  732. 'Football',
  733. 'Running Shoe',
  734. ]);
  735. const result2 = await doAdminSearchQuery({ collectionSlug: 'plants', groupByProduct: true });
  736. expect(result2.search.items.map(i => i.productName)).toEqual([
  737. 'Road Bike',
  738. 'Skipping Rope',
  739. 'Boxing Gloves',
  740. 'Tent',
  741. 'Cruiser Skateboard',
  742. 'Football',
  743. 'Running Shoe',
  744. ]);
  745. }, 10000);
  746. it('updates index when a Collection created', async () => {
  747. const { createCollection } = await adminClient.query<
  748. CreateCollection.Mutation,
  749. CreateCollection.Variables
  750. >(CREATE_COLLECTION, {
  751. input: {
  752. translations: [
  753. {
  754. languageCode: LanguageCode.en,
  755. name: 'Photo',
  756. description: '',
  757. slug: 'photo',
  758. },
  759. ],
  760. filters: [
  761. {
  762. code: facetValueCollectionFilter.code,
  763. arguments: [
  764. {
  765. name: 'facetValueIds',
  766. value: `["T_3"]`,
  767. },
  768. {
  769. name: 'containsAny',
  770. value: `false`,
  771. },
  772. ],
  773. },
  774. ],
  775. },
  776. });
  777. await awaitRunningJobs(adminClient);
  778. // add an additional check for the collection filters to update
  779. await awaitRunningJobs(adminClient);
  780. const result = await doAdminSearchQuery({
  781. collectionId: createCollection.id,
  782. groupByProduct: true,
  783. });
  784. expect(result.search.items.map(i => i.productName)).toEqual([
  785. 'Instant Camera',
  786. 'Camera Lens',
  787. 'Tripod',
  788. 'Slr Camera',
  789. ]);
  790. });
  791. it('updates index when a taxRate is changed', async () => {
  792. await adminClient.query<UpdateTaxRate.Mutation, UpdateTaxRate.Variables>(UPDATE_TAX_RATE, {
  793. input: {
  794. // Default Channel's defaultTaxZone is Europe (id 2) and the id of the standard TaxRate
  795. // to Europe is 2.
  796. id: 'T_2',
  797. value: 50,
  798. },
  799. });
  800. await awaitRunningJobs(adminClient);
  801. const result = await adminClient.query<SearchGetPrices.Query, SearchGetPrices.Variables>(
  802. SEARCH_GET_PRICES,
  803. {
  804. input: {
  805. groupByProduct: true,
  806. term: 'laptop',
  807. } as SearchInput,
  808. },
  809. );
  810. expect(result.search.items).toEqual([
  811. {
  812. price: { min: 129900, max: 229900 },
  813. priceWithTax: { min: 194850, max: 344850 },
  814. },
  815. ]);
  816. });
  817. describe('asset changes', () => {
  818. function searchForLaptop() {
  819. return adminClient.query<SearchGetAssets.Query, SearchGetAssets.Variables>(
  820. SEARCH_GET_ASSETS,
  821. {
  822. input: {
  823. term: 'laptop',
  824. take: 1,
  825. },
  826. },
  827. );
  828. }
  829. it('updates index when asset focalPoint is changed', async () => {
  830. const { search: search1 } = await searchForLaptop();
  831. expect(search1.items[0].productAsset!.id).toBe('T_1');
  832. expect(search1.items[0].productAsset!.focalPoint).toBeNull();
  833. await adminClient.query<UpdateAsset.Mutation, UpdateAsset.Variables>(UPDATE_ASSET, {
  834. input: {
  835. id: 'T_1',
  836. focalPoint: {
  837. x: 0.42,
  838. y: 0.42,
  839. },
  840. },
  841. });
  842. await awaitRunningJobs(adminClient);
  843. const { search: search2 } = await searchForLaptop();
  844. expect(search2.items[0].productAsset!.id).toBe('T_1');
  845. expect(search2.items[0].productAsset!.focalPoint).toEqual({ x: 0.42, y: 0.42 });
  846. });
  847. it('updates index when asset deleted', async () => {
  848. const { search: search1 } = await searchForLaptop();
  849. const assetId = search1.items[0].productAsset?.id;
  850. expect(assetId).toBeTruthy();
  851. await adminClient.query<DeleteAsset.Mutation, DeleteAsset.Variables>(DELETE_ASSET, {
  852. input: {
  853. assetId: assetId!,
  854. force: true,
  855. },
  856. });
  857. await awaitRunningJobs(adminClient);
  858. const { search: search2 } = await searchForLaptop();
  859. expect(search2.items[0].productAsset).toBeNull();
  860. });
  861. });
  862. it('does not include deleted ProductVariants in index', async () => {
  863. const { search: s1 } = await doAdminSearchQuery({
  864. term: 'hard drive',
  865. groupByProduct: false,
  866. });
  867. const { deleteProductVariant } = await adminClient.query<
  868. DeleteProductVariant.Mutation,
  869. DeleteProductVariant.Variables
  870. >(DELETE_PRODUCT_VARIANT, { id: s1.items[0].productVariantId });
  871. await awaitRunningJobs(adminClient);
  872. const { search } = await adminClient.query<SearchGetPrices.Query, SearchGetPrices.Variables>(
  873. SEARCH_GET_PRICES,
  874. { input: { term: 'hard drive', groupByProduct: true } },
  875. );
  876. expect(search.items[0].price).toEqual({
  877. min: 7896,
  878. max: 13435,
  879. });
  880. });
  881. it('returns enabled field when not grouped', async () => {
  882. const result = await doAdminSearchQuery({ groupByProduct: false, take: 3 });
  883. expect(result.search.items.map(pick(['productVariantId', 'enabled']))).toEqual([
  884. { productVariantId: 'T_1', enabled: true },
  885. { productVariantId: 'T_2', enabled: true },
  886. { productVariantId: 'T_3', enabled: false },
  887. ]);
  888. });
  889. it('when grouped, enabled is true if at least one variant is enabled', async () => {
  890. await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
  891. UPDATE_PRODUCT_VARIANTS,
  892. {
  893. input: [
  894. { id: 'T_1', enabled: false },
  895. { id: 'T_2', enabled: false },
  896. ],
  897. },
  898. );
  899. await awaitRunningJobs(adminClient);
  900. const result = await doAdminSearchQuery({ groupByProduct: true, take: 3 });
  901. expect(result.search.items.map(pick(['productId', 'enabled']))).toEqual([
  902. { productId: 'T_1', enabled: true },
  903. { productId: 'T_2', enabled: true },
  904. { productId: 'T_3', enabled: true },
  905. ]);
  906. });
  907. it('when grouped, enabled is false if all variants are disabled', async () => {
  908. await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
  909. UPDATE_PRODUCT_VARIANTS,
  910. {
  911. input: [{ id: 'T_4', enabled: false }],
  912. },
  913. );
  914. await awaitRunningJobs(adminClient);
  915. const result = await doAdminSearchQuery({ groupByProduct: true, take: 3 });
  916. expect(result.search.items.map(pick(['productId', 'enabled']))).toEqual([
  917. { productId: 'T_1', enabled: false },
  918. { productId: 'T_2', enabled: true },
  919. { productId: 'T_3', enabled: true },
  920. ]);
  921. });
  922. it('when grouped, enabled is false if product is disabled', async () => {
  923. await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
  924. input: {
  925. id: 'T_3',
  926. enabled: false,
  927. },
  928. });
  929. await awaitRunningJobs(adminClient);
  930. const result = await doAdminSearchQuery({ groupByProduct: true, take: 3 });
  931. expect(result.search.items.map(pick(['productId', 'enabled']))).toEqual([
  932. { productId: 'T_1', enabled: false },
  933. { productId: 'T_2', enabled: true },
  934. { productId: 'T_3', enabled: false },
  935. ]);
  936. });
  937. // https://github.com/vendure-ecommerce/vendure/issues/295
  938. it('enabled status survives reindex', async () => {
  939. await adminClient.query<Reindex.Mutation>(REINDEX);
  940. await awaitRunningJobs(adminClient);
  941. const result = await doAdminSearchQuery({ groupByProduct: true, take: 3 });
  942. expect(result.search.items.map(pick(['productId', 'enabled']))).toEqual([
  943. { productId: 'T_1', enabled: false },
  944. { productId: 'T_2', enabled: true },
  945. { productId: 'T_3', enabled: false },
  946. ]);
  947. });
  948. // https://github.com/vendure-ecommerce/vendure/issues/745
  949. it('very long Product descriptions no not cause indexing to fail', async () => {
  950. // We generate this long string out of random chars because Postgres uses compression
  951. // when storing the string value, so e.g. a long series of a single character will not
  952. // reproduce the error.
  953. const description = Array.from({ length: 220 })
  954. .map(() => Math.random().toString(36))
  955. .join(' ');
  956. const { createProduct } = await adminClient.query<
  957. CreateProduct.Mutation,
  958. CreateProduct.Variables
  959. >(CREATE_PRODUCT, {
  960. input: {
  961. translations: [
  962. {
  963. languageCode: LanguageCode.en,
  964. name: 'Very long description aabbccdd',
  965. slug: 'very-long-description',
  966. description,
  967. },
  968. ],
  969. },
  970. });
  971. await adminClient.query<CreateProductVariants.Mutation, CreateProductVariants.Variables>(
  972. CREATE_PRODUCT_VARIANTS,
  973. {
  974. input: [
  975. {
  976. productId: createProduct.id,
  977. sku: 'VLD01',
  978. price: 100,
  979. translations: [
  980. { languageCode: LanguageCode.en, name: 'Very long description variant' },
  981. ],
  982. },
  983. ],
  984. },
  985. );
  986. await awaitRunningJobs(adminClient);
  987. const result = await doAdminSearchQuery({ term: 'aabbccdd' });
  988. expect(result.search.items.map(i => i.productName)).toEqual([
  989. 'Very long description aabbccdd',
  990. ]);
  991. await adminClient.query<DeleteProduct.Mutation, DeleteProduct.Variables>(DELETE_PRODUCT, {
  992. id: createProduct.id,
  993. });
  994. });
  995. });
  996. // https://github.com/vendure-ecommerce/vendure/issues/609
  997. describe('Synthetic index items', () => {
  998. let createdProductId: string;
  999. it('creates synthetic index item for Product with no variants', async () => {
  1000. const { createProduct } = await adminClient.query<
  1001. CreateProduct.Mutation,
  1002. CreateProduct.Variables
  1003. >(CREATE_PRODUCT, {
  1004. input: {
  1005. facetValueIds: ['T_1'],
  1006. translations: [
  1007. {
  1008. languageCode: LanguageCode.en,
  1009. name: 'Strawberry cheesecake',
  1010. slug: 'strawberry-cheesecake',
  1011. description: 'A yummy dessert',
  1012. },
  1013. ],
  1014. },
  1015. });
  1016. await awaitRunningJobs(adminClient);
  1017. const result = await doAdminSearchQuery({ groupByProduct: true, term: 'strawberry' });
  1018. expect(
  1019. result.search.items.map(
  1020. pick([
  1021. 'productId',
  1022. 'enabled',
  1023. 'productName',
  1024. 'productVariantName',
  1025. 'slug',
  1026. 'description',
  1027. ]),
  1028. ),
  1029. ).toEqual([
  1030. {
  1031. productId: createProduct.id,
  1032. enabled: false,
  1033. productName: 'Strawberry cheesecake',
  1034. productVariantName: 'Strawberry cheesecake',
  1035. slug: 'strawberry-cheesecake',
  1036. description: 'A yummy dessert',
  1037. },
  1038. ]);
  1039. createdProductId = createProduct.id;
  1040. });
  1041. it('removes synthetic index item once a variant is created', async () => {
  1042. const { createProductVariants } = await adminClient.query<
  1043. CreateProductVariants.Mutation,
  1044. CreateProductVariants.Variables
  1045. >(CREATE_PRODUCT_VARIANTS, {
  1046. input: [
  1047. {
  1048. productId: createdProductId,
  1049. sku: 'SC01',
  1050. price: 1399,
  1051. translations: [
  1052. { languageCode: LanguageCode.en, name: 'Strawberry Cheesecake Pie' },
  1053. ],
  1054. },
  1055. ],
  1056. });
  1057. await awaitRunningJobs(adminClient);
  1058. const result = await doAdminSearchQuery({ groupByProduct: false, term: 'strawberry' });
  1059. expect(result.search.items.map(pick(['productVariantName']))).toEqual([
  1060. { productVariantName: 'Strawberry Cheesecake Pie' },
  1061. ]);
  1062. });
  1063. });
  1064. describe('channel handling', () => {
  1065. const SECOND_CHANNEL_TOKEN = 'second-channel-token';
  1066. let secondChannel: ChannelFragment;
  1067. beforeAll(async () => {
  1068. const { createChannel } = await adminClient.query<
  1069. CreateChannel.Mutation,
  1070. CreateChannel.Variables
  1071. >(CREATE_CHANNEL, {
  1072. input: {
  1073. code: 'second-channel',
  1074. token: SECOND_CHANNEL_TOKEN,
  1075. defaultLanguageCode: LanguageCode.en,
  1076. currencyCode: CurrencyCode.GBP,
  1077. pricesIncludeTax: true,
  1078. defaultTaxZoneId: 'T_1',
  1079. defaultShippingZoneId: 'T_1',
  1080. },
  1081. });
  1082. secondChannel = createChannel as ChannelFragment;
  1083. });
  1084. it('adding product to channel', async () => {
  1085. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  1086. await adminClient.query<AssignProductsToChannel.Mutation, AssignProductsToChannel.Variables>(
  1087. ASSIGN_PRODUCT_TO_CHANNEL,
  1088. {
  1089. input: { channelId: secondChannel.id, productIds: ['T_1', 'T_2'] },
  1090. },
  1091. );
  1092. await awaitRunningJobs(adminClient);
  1093. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  1094. const { search } = await doAdminSearchQuery({ groupByProduct: true });
  1095. expect(search.items.map(i => i.productId)).toEqual(['T_1', 'T_2']);
  1096. }, 10000);
  1097. it('removing product from channel', async () => {
  1098. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  1099. const { removeProductsFromChannel } = await adminClient.query<
  1100. RemoveProductsFromChannel.Mutation,
  1101. RemoveProductsFromChannel.Variables
  1102. >(REMOVE_PRODUCT_FROM_CHANNEL, {
  1103. input: {
  1104. productIds: ['T_2'],
  1105. channelId: secondChannel.id,
  1106. },
  1107. });
  1108. await awaitRunningJobs(adminClient);
  1109. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  1110. const { search } = await doAdminSearchQuery({ groupByProduct: true });
  1111. expect(search.items.map(i => i.productId)).toEqual(['T_1']);
  1112. }, 10000);
  1113. it('adding product variant to channel', async () => {
  1114. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  1115. await adminClient.query<
  1116. AssignProductVariantsToChannel.Mutation,
  1117. AssignProductVariantsToChannel.Variables
  1118. >(ASSIGN_PRODUCTVARIANT_TO_CHANNEL, {
  1119. input: { channelId: secondChannel.id, productVariantIds: ['T_10', 'T_15'] },
  1120. });
  1121. await awaitRunningJobs(adminClient);
  1122. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  1123. const { search: searchGrouped } = await doAdminSearchQuery({ groupByProduct: true });
  1124. expect(searchGrouped.items.map(i => i.productId)).toEqual(['T_1', 'T_3', 'T_4']);
  1125. const { search: searchUngrouped } = await doAdminSearchQuery({ groupByProduct: false });
  1126. expect(searchUngrouped.items.map(i => i.productVariantId)).toEqual([
  1127. 'T_1',
  1128. 'T_2',
  1129. 'T_3',
  1130. 'T_4',
  1131. 'T_10',
  1132. 'T_15',
  1133. ]);
  1134. }, 10000);
  1135. it('removing product variant from channel', async () => {
  1136. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  1137. await adminClient.query<
  1138. RemoveProductVariantsFromChannel.Mutation,
  1139. RemoveProductVariantsFromChannel.Variables
  1140. >(REMOVE_PRODUCTVARIANT_FROM_CHANNEL, {
  1141. input: { channelId: secondChannel.id, productVariantIds: ['T_1', 'T_15'] },
  1142. });
  1143. await awaitRunningJobs(adminClient);
  1144. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  1145. const { search: searchGrouped } = await doAdminSearchQuery({ groupByProduct: true });
  1146. expect(searchGrouped.items.map(i => i.productId)).toEqual(['T_1', 'T_3']);
  1147. const { search: searchUngrouped } = await doAdminSearchQuery({ groupByProduct: false });
  1148. expect(searchUngrouped.items.map(i => i.productVariantId)).toEqual([
  1149. 'T_2',
  1150. 'T_3',
  1151. 'T_4',
  1152. 'T_10',
  1153. ]);
  1154. }, 10000);
  1155. it('updating product affects current channel', async () => {
  1156. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  1157. const { updateProduct } = await adminClient.query<
  1158. UpdateProduct.Mutation,
  1159. UpdateProduct.Variables
  1160. >(UPDATE_PRODUCT, {
  1161. input: {
  1162. id: 'T_3',
  1163. enabled: true,
  1164. translations: [{ languageCode: LanguageCode.en, name: 'xyz' }],
  1165. },
  1166. });
  1167. await awaitRunningJobs(adminClient);
  1168. const { search: searchGrouped } = await doAdminSearchQuery({
  1169. groupByProduct: true,
  1170. term: 'xyz',
  1171. });
  1172. expect(searchGrouped.items.map(i => i.productName)).toEqual(['xyz']);
  1173. });
  1174. it('updating product affects other channels', async () => {
  1175. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  1176. const { search: searchGrouped } = await doAdminSearchQuery({
  1177. groupByProduct: true,
  1178. term: 'xyz',
  1179. });
  1180. expect(searchGrouped.items.map(i => i.productName)).toEqual(['xyz']);
  1181. });
  1182. // https://github.com/vendure-ecommerce/vendure/issues/896
  1183. it('removing from channel with multiple languages', async () => {
  1184. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  1185. await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
  1186. input: {
  1187. id: 'T_4',
  1188. translations: [
  1189. {
  1190. languageCode: LanguageCode.en,
  1191. name: 'product en',
  1192. slug: 'product-en',
  1193. description: 'en',
  1194. },
  1195. {
  1196. languageCode: LanguageCode.de,
  1197. name: 'product de',
  1198. slug: 'product-de',
  1199. description: 'de',
  1200. },
  1201. ],
  1202. },
  1203. });
  1204. await adminClient.query<AssignProductsToChannel.Mutation, AssignProductsToChannel.Variables>(
  1205. ASSIGN_PRODUCT_TO_CHANNEL,
  1206. {
  1207. input: { channelId: secondChannel.id, productIds: ['T_4'] },
  1208. },
  1209. );
  1210. await awaitRunningJobs(adminClient);
  1211. async function searchSecondChannelForDEProduct() {
  1212. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  1213. const { search } = await adminClient.query<
  1214. SearchProductsShop.Query,
  1215. SearchProductsShop.Variables
  1216. >(
  1217. SEARCH_PRODUCTS,
  1218. {
  1219. input: { term: 'product', groupByProduct: true },
  1220. },
  1221. { languageCode: LanguageCode.de },
  1222. );
  1223. return search;
  1224. }
  1225. const search1 = await searchSecondChannelForDEProduct();
  1226. expect(search1.items.map(i => i.productName)).toEqual(['product de']);
  1227. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  1228. const { removeProductsFromChannel } = await adminClient.query<
  1229. RemoveProductsFromChannel.Mutation,
  1230. RemoveProductsFromChannel.Variables
  1231. >(REMOVE_PRODUCT_FROM_CHANNEL, {
  1232. input: {
  1233. productIds: ['T_4'],
  1234. channelId: secondChannel.id,
  1235. },
  1236. });
  1237. await awaitRunningJobs(adminClient);
  1238. const search2 = await searchSecondChannelForDEProduct();
  1239. expect(search2.items.map(i => i.productName)).toEqual([]);
  1240. });
  1241. });
  1242. describe('multiple language handling', () => {
  1243. function searchInLanguage(languageCode: LanguageCode) {
  1244. return adminClient.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  1245. SEARCH_PRODUCTS,
  1246. {
  1247. input: {
  1248. take: 1,
  1249. },
  1250. },
  1251. {
  1252. languageCode,
  1253. },
  1254. );
  1255. }
  1256. beforeAll(async () => {
  1257. const { updateProduct } = await adminClient.query<
  1258. UpdateProduct.Mutation,
  1259. UpdateProduct.Variables
  1260. >(UPDATE_PRODUCT, {
  1261. input: {
  1262. id: 'T_1',
  1263. translations: [
  1264. {
  1265. languageCode: LanguageCode.de,
  1266. name: 'laptop name de',
  1267. slug: 'laptop-slug-de',
  1268. description: 'laptop description de',
  1269. },
  1270. {
  1271. languageCode: LanguageCode.zh,
  1272. name: 'laptop name zh',
  1273. slug: 'laptop-slug-zh',
  1274. description: 'laptop description zh',
  1275. },
  1276. ],
  1277. },
  1278. });
  1279. await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
  1280. UPDATE_PRODUCT_VARIANTS,
  1281. {
  1282. input: [
  1283. {
  1284. id: updateProduct.variants[0].id,
  1285. translations: [
  1286. {
  1287. languageCode: LanguageCode.fr,
  1288. name: 'laptop variant fr',
  1289. },
  1290. ],
  1291. },
  1292. ],
  1293. },
  1294. );
  1295. await awaitRunningJobs(adminClient);
  1296. });
  1297. it('indexes product-level languages', async () => {
  1298. const { search: search1 } = await searchInLanguage(LanguageCode.de);
  1299. expect(search1.items[0].productName).toBe('laptop name de');
  1300. expect(search1.items[0].slug).toBe('laptop-slug-de');
  1301. expect(search1.items[0].description).toBe('laptop description de');
  1302. const { search: search2 } = await searchInLanguage(LanguageCode.zh);
  1303. expect(search2.items[0].productName).toBe('laptop name zh');
  1304. expect(search2.items[0].slug).toBe('laptop-slug-zh');
  1305. expect(search2.items[0].description).toBe('laptop description zh');
  1306. });
  1307. it('indexes product variant-level languages', async () => {
  1308. const { search: search1 } = await searchInLanguage(LanguageCode.fr);
  1309. expect(search1.items[0].productName).toBe('Laptop');
  1310. expect(search1.items[0].productVariantName).toBe('laptop variant fr');
  1311. });
  1312. });
  1313. });
  1314. });
  1315. export const REINDEX = gql`
  1316. mutation Reindex {
  1317. reindex {
  1318. id
  1319. }
  1320. }
  1321. `;
  1322. export const SEARCH_PRODUCTS = gql`
  1323. query SearchProductsAdmin($input: SearchInput!) {
  1324. search(input: $input) {
  1325. totalItems
  1326. items {
  1327. enabled
  1328. productId
  1329. productName
  1330. slug
  1331. description
  1332. productVariantId
  1333. productVariantName
  1334. sku
  1335. }
  1336. }
  1337. }
  1338. `;
  1339. export const SEARCH_GET_FACET_VALUES = gql`
  1340. query SearchFacetValues($input: SearchInput!) {
  1341. search(input: $input) {
  1342. totalItems
  1343. facetValues {
  1344. count
  1345. facetValue {
  1346. id
  1347. name
  1348. }
  1349. }
  1350. }
  1351. }
  1352. `;
  1353. export const SEARCH_GET_COLLECTIONS = gql`
  1354. query SearchCollections($input: SearchInput!) {
  1355. search(input: $input) {
  1356. totalItems
  1357. collections {
  1358. count
  1359. collection {
  1360. id
  1361. name
  1362. }
  1363. }
  1364. }
  1365. }
  1366. `;
  1367. export const SEARCH_GET_ASSETS = gql`
  1368. query SearchGetAssets($input: SearchInput!) {
  1369. search(input: $input) {
  1370. totalItems
  1371. items {
  1372. productId
  1373. productName
  1374. productVariantName
  1375. productAsset {
  1376. id
  1377. preview
  1378. focalPoint {
  1379. x
  1380. y
  1381. }
  1382. }
  1383. productVariantAsset {
  1384. id
  1385. preview
  1386. focalPoint {
  1387. x
  1388. y
  1389. }
  1390. }
  1391. }
  1392. }
  1393. }
  1394. `;
  1395. export const SEARCH_GET_PRICES = gql`
  1396. query SearchGetPrices($input: SearchInput!) {
  1397. search(input: $input) {
  1398. items {
  1399. price {
  1400. ... on PriceRange {
  1401. min
  1402. max
  1403. }
  1404. ... on SinglePrice {
  1405. value
  1406. }
  1407. }
  1408. priceWithTax {
  1409. ... on PriceRange {
  1410. min
  1411. max
  1412. }
  1413. ... on SinglePrice {
  1414. value
  1415. }
  1416. }
  1417. }
  1418. }
  1419. }
  1420. `;