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

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