elasticsearch-plugin.e2e-spec.ts 61 KB

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