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

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