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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648
  1. import { pick } from '@vendure/common/lib/pick';
  2. import { mergeConfig } from '@vendure/core';
  3. import { DefaultSearchPlugin } from '@vendure/core';
  4. import { facetValueCollectionFilter } from '@vendure/core/dist/config/collection/default-collection-filters';
  5. import { createTestEnvironment, TestClient } from '@vendure/testing';
  6. import gql from 'graphql-tag';
  7. import path from 'path';
  8. import { dataDir, TEST_SETUP_TIMEOUT_MS, testConfig } from './config/test-config';
  9. import { initialData } from './fixtures/e2e-initial-data';
  10. import {
  11. CreateCollection,
  12. CreateFacet,
  13. LanguageCode,
  14. SearchFacetValues,
  15. SearchGetPrices,
  16. SearchInput,
  17. UpdateCollection,
  18. UpdateProduct,
  19. UpdateProductVariants,
  20. UpdateTaxRate,
  21. } from './graphql/generated-e2e-admin-types';
  22. import { SearchProductsShop } from './graphql/generated-e2e-shop-types';
  23. import {
  24. CREATE_COLLECTION,
  25. CREATE_FACET,
  26. UPDATE_COLLECTION,
  27. UPDATE_PRODUCT,
  28. UPDATE_PRODUCT_VARIANTS,
  29. UPDATE_TAX_RATE,
  30. } from './graphql/shared-definitions';
  31. import { SEARCH_PRODUCTS_SHOP } from './graphql/shop-definitions';
  32. import { awaitRunningJobs } from './utils/await-running-jobs';
  33. describe('Default search plugin', () => {
  34. const { server, adminClient, shopClient } = createTestEnvironment(
  35. mergeConfig(testConfig, { plugins: [DefaultSearchPlugin] }),
  36. );
  37. beforeAll(async () => {
  38. const token = await server.init({
  39. dataDir,
  40. initialData,
  41. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
  42. customerCount: 1,
  43. });
  44. await adminClient.init();
  45. await adminClient.asSuperAdmin();
  46. await shopClient.init();
  47. }, TEST_SETUP_TIMEOUT_MS);
  48. afterAll(async () => {
  49. await server.destroy();
  50. });
  51. async function testGroupByProduct(client: TestClient) {
  52. const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  53. SEARCH_PRODUCTS_SHOP,
  54. {
  55. input: {
  56. groupByProduct: true,
  57. },
  58. },
  59. );
  60. expect(result.search.totalItems).toBe(20);
  61. }
  62. async function testNoGrouping(client: TestClient) {
  63. const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  64. SEARCH_PRODUCTS_SHOP,
  65. {
  66. input: {
  67. groupByProduct: false,
  68. },
  69. },
  70. );
  71. expect(result.search.totalItems).toBe(34);
  72. }
  73. async function testMatchSearchTerm(client: TestClient) {
  74. const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  75. SEARCH_PRODUCTS_SHOP,
  76. {
  77. input: {
  78. term: 'camera',
  79. groupByProduct: true,
  80. },
  81. },
  82. );
  83. expect(result.search.items.map(i => i.productName)).toEqual([
  84. 'Instant Camera',
  85. 'Camera Lens',
  86. 'SLR Camera',
  87. ]);
  88. }
  89. async function testMatchFacetIds(client: TestClient) {
  90. const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  91. SEARCH_PRODUCTS_SHOP,
  92. {
  93. input: {
  94. facetValueIds: ['T_1', 'T_2'],
  95. groupByProduct: true,
  96. },
  97. },
  98. );
  99. expect(result.search.items.map(i => i.productName)).toEqual([
  100. 'Laptop',
  101. 'Curvy Monitor',
  102. 'Gaming PC',
  103. 'Hard Drive',
  104. 'Clacky Keyboard',
  105. 'USB Cable',
  106. ]);
  107. }
  108. async function testMatchCollectionId(client: TestClient) {
  109. const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  110. SEARCH_PRODUCTS_SHOP,
  111. {
  112. input: {
  113. collectionId: 'T_2',
  114. groupByProduct: true,
  115. },
  116. },
  117. );
  118. expect(result.search.items.map(i => i.productName)).toEqual([
  119. 'Spiky Cactus',
  120. 'Orchid',
  121. 'Bonsai Tree',
  122. ]);
  123. }
  124. async function testSinglePrices(client: TestClient) {
  125. const result = await client.query<SearchGetPrices.Query, SearchGetPrices.Variables>(
  126. SEARCH_GET_PRICES,
  127. {
  128. input: {
  129. groupByProduct: false,
  130. take: 3,
  131. } as SearchInput,
  132. },
  133. );
  134. expect(result.search.items).toEqual([
  135. {
  136. price: { value: 129900 },
  137. priceWithTax: { value: 155880 },
  138. },
  139. {
  140. price: { value: 139900 },
  141. priceWithTax: { value: 167880 },
  142. },
  143. {
  144. price: { value: 219900 },
  145. priceWithTax: { value: 263880 },
  146. },
  147. ]);
  148. }
  149. async function testPriceRanges(client: TestClient) {
  150. const result = await client.query<SearchGetPrices.Query, SearchGetPrices.Variables>(
  151. SEARCH_GET_PRICES,
  152. {
  153. input: {
  154. groupByProduct: true,
  155. take: 3,
  156. } as SearchInput,
  157. },
  158. );
  159. expect(result.search.items).toEqual([
  160. {
  161. price: { min: 129900, max: 229900 },
  162. priceWithTax: { min: 155880, max: 275880 },
  163. },
  164. {
  165. price: { min: 14374, max: 16994 },
  166. priceWithTax: { min: 17249, max: 20393 },
  167. },
  168. {
  169. price: { min: 93120, max: 109995 },
  170. priceWithTax: { min: 111744, max: 131994 },
  171. },
  172. ]);
  173. }
  174. describe('shop api', () => {
  175. it('group by product', () => testGroupByProduct(shopClient));
  176. it('no grouping', () => testNoGrouping(shopClient));
  177. it('matches search term', () => testMatchSearchTerm(shopClient));
  178. it('matches by facetId', () => testMatchFacetIds(shopClient));
  179. it('matches by collectionId', () => testMatchCollectionId(shopClient));
  180. it('single prices', () => testSinglePrices(shopClient));
  181. it('price ranges', () => testPriceRanges(shopClient));
  182. it('returns correct facetValues when not grouped by product', async () => {
  183. const result = await shopClient.query<SearchFacetValues.Query, SearchFacetValues.Variables>(
  184. SEARCH_GET_FACET_VALUES,
  185. {
  186. input: {
  187. groupByProduct: false,
  188. },
  189. },
  190. );
  191. expect(result.search.facetValues).toEqual([
  192. { count: 21, facetValue: { id: 'T_1', name: 'electronics' } },
  193. { count: 17, facetValue: { id: 'T_2', name: 'computers' } },
  194. { count: 4, facetValue: { id: 'T_3', name: 'photo' } },
  195. { count: 10, facetValue: { id: 'T_4', name: 'sports equipment' } },
  196. { count: 3, facetValue: { id: 'T_5', name: 'home & garden' } },
  197. { count: 3, facetValue: { id: 'T_6', name: 'plants' } },
  198. ]);
  199. });
  200. it('returns correct facetValues when grouped by product', async () => {
  201. const result = await shopClient.query<SearchFacetValues.Query, SearchFacetValues.Variables>(
  202. SEARCH_GET_FACET_VALUES,
  203. {
  204. input: {
  205. groupByProduct: true,
  206. },
  207. },
  208. );
  209. expect(result.search.facetValues).toEqual([
  210. { count: 10, facetValue: { id: 'T_1', name: 'electronics' } },
  211. { count: 6, facetValue: { id: 'T_2', name: 'computers' } },
  212. { count: 4, facetValue: { id: 'T_3', name: 'photo' } },
  213. { count: 7, facetValue: { id: 'T_4', name: 'sports equipment' } },
  214. { count: 3, facetValue: { id: 'T_5', name: 'home & garden' } },
  215. { count: 3, facetValue: { id: 'T_6', name: 'plants' } },
  216. ]);
  217. });
  218. it('omits facetValues of private facets', async () => {
  219. const { createFacet } = await adminClient.query<CreateFacet.Mutation, CreateFacet.Variables>(
  220. CREATE_FACET,
  221. {
  222. input: {
  223. code: 'profit-margin',
  224. isPrivate: true,
  225. translations: [{ languageCode: LanguageCode.en, name: 'Profit Margin' }],
  226. values: [
  227. {
  228. code: 'massive',
  229. translations: [{ languageCode: LanguageCode.en, name: 'massive' }],
  230. },
  231. ],
  232. },
  233. },
  234. );
  235. await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
  236. input: {
  237. id: 'T_2',
  238. // T_1 & T_2 are the existing facetValues (electronics & photo)
  239. facetValueIds: ['T_1', 'T_2', createFacet.values[0].id],
  240. },
  241. });
  242. const result = await shopClient.query<SearchFacetValues.Query, SearchFacetValues.Variables>(
  243. SEARCH_GET_FACET_VALUES,
  244. {
  245. input: {
  246. groupByProduct: true,
  247. },
  248. },
  249. );
  250. expect(result.search.facetValues).toEqual([
  251. { count: 10, facetValue: { id: 'T_1', name: 'electronics' } },
  252. { count: 6, facetValue: { id: 'T_2', name: 'computers' } },
  253. { count: 4, facetValue: { id: 'T_3', name: 'photo' } },
  254. { count: 7, facetValue: { id: 'T_4', name: 'sports equipment' } },
  255. { count: 3, facetValue: { id: 'T_5', name: 'home & garden' } },
  256. { count: 3, facetValue: { id: 'T_6', name: 'plants' } },
  257. ]);
  258. });
  259. it('encodes the productId and productVariantId', async () => {
  260. const result = await shopClient.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  261. SEARCH_PRODUCTS_SHOP,
  262. {
  263. input: {
  264. groupByProduct: false,
  265. take: 1,
  266. },
  267. },
  268. );
  269. expect(pick(result.search.items[0], ['productId', 'productVariantId'])).toEqual({
  270. productId: 'T_1',
  271. productVariantId: 'T_1',
  272. });
  273. });
  274. it('omits results for disabled ProductVariants', async () => {
  275. await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
  276. UPDATE_PRODUCT_VARIANTS,
  277. {
  278. input: [{ id: 'T_3', enabled: false }],
  279. },
  280. );
  281. await awaitRunningJobs(adminClient);
  282. const result = await shopClient.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  283. SEARCH_PRODUCTS_SHOP,
  284. {
  285. input: {
  286. groupByProduct: false,
  287. take: 3,
  288. },
  289. },
  290. );
  291. expect(result.search.items.map(i => i.productVariantId)).toEqual(['T_1', 'T_2', 'T_4']);
  292. });
  293. it('encodes collectionIds', async () => {
  294. const result = await shopClient.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  295. SEARCH_PRODUCTS_SHOP,
  296. {
  297. input: {
  298. groupByProduct: false,
  299. term: 'cactus',
  300. take: 1,
  301. },
  302. },
  303. );
  304. expect(result.search.items[0].collectionIds).toEqual(['T_2']);
  305. });
  306. });
  307. describe('admin api', () => {
  308. it('group by product', () => testGroupByProduct(adminClient));
  309. it('no grouping', () => testNoGrouping(adminClient));
  310. it('matches search term', () => testMatchSearchTerm(adminClient));
  311. it('matches by facetId', () => testMatchFacetIds(adminClient));
  312. it('matches by collectionId', () => testMatchCollectionId(adminClient));
  313. it('single prices', () => testSinglePrices(adminClient));
  314. it('price ranges', () => testPriceRanges(adminClient));
  315. it('updates index when a Product is changed', async () => {
  316. await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
  317. input: {
  318. id: 'T_1',
  319. facetValueIds: [],
  320. },
  321. });
  322. await awaitRunningJobs(adminClient);
  323. const result = await adminClient.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  324. SEARCH_PRODUCTS,
  325. {
  326. input: {
  327. facetValueIds: ['T_2'],
  328. groupByProduct: true,
  329. },
  330. },
  331. );
  332. expect(result.search.items.map(i => i.productName)).toEqual([
  333. 'Curvy Monitor',
  334. 'Gaming PC',
  335. 'Hard Drive',
  336. 'Clacky Keyboard',
  337. 'USB Cable',
  338. ]);
  339. });
  340. it('updates index when a Collection is changed', async () => {
  341. await adminClient.query<UpdateCollection.Mutation, UpdateCollection.Variables>(
  342. UPDATE_COLLECTION,
  343. {
  344. input: {
  345. id: 'T_2',
  346. filters: [
  347. {
  348. code: facetValueCollectionFilter.code,
  349. arguments: [
  350. {
  351. name: 'facetValueIds',
  352. value: `["T_4"]`,
  353. type: 'facetValueIds',
  354. },
  355. {
  356. name: 'containsAny',
  357. value: `false`,
  358. type: 'boolean',
  359. },
  360. ],
  361. },
  362. ],
  363. },
  364. },
  365. );
  366. await awaitRunningJobs(adminClient);
  367. const result = await adminClient.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  368. SEARCH_PRODUCTS,
  369. {
  370. input: {
  371. collectionId: 'T_2',
  372. groupByProduct: true,
  373. },
  374. },
  375. );
  376. expect(result.search.items.map(i => i.productName)).toEqual([
  377. 'Road Bike',
  378. 'Skipping Rope',
  379. 'Boxing Gloves',
  380. 'Tent',
  381. 'Cruiser Skateboard',
  382. 'Football',
  383. 'Running Shoe',
  384. ]);
  385. });
  386. it('updates index when a Collection created', async () => {
  387. const { createCollection } = await adminClient.query<
  388. CreateCollection.Mutation,
  389. CreateCollection.Variables
  390. >(CREATE_COLLECTION, {
  391. input: {
  392. translations: [
  393. {
  394. languageCode: LanguageCode.en,
  395. name: 'Photo',
  396. description: '',
  397. },
  398. ],
  399. filters: [
  400. {
  401. code: facetValueCollectionFilter.code,
  402. arguments: [
  403. {
  404. name: 'facetValueIds',
  405. value: `["T_3"]`,
  406. type: 'facetValueIds',
  407. },
  408. {
  409. name: 'containsAny',
  410. value: `false`,
  411. type: 'boolean',
  412. },
  413. ],
  414. },
  415. ],
  416. },
  417. });
  418. await awaitRunningJobs(adminClient);
  419. const result = await adminClient.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  420. SEARCH_PRODUCTS,
  421. {
  422. input: {
  423. collectionId: createCollection.id,
  424. groupByProduct: true,
  425. },
  426. },
  427. );
  428. expect(result.search.items.map(i => i.productName)).toEqual([
  429. 'Instant Camera',
  430. 'Camera Lens',
  431. 'Tripod',
  432. 'SLR Camera',
  433. ]);
  434. });
  435. it('updates index when a taxRate is changed', async () => {
  436. await adminClient.query<UpdateTaxRate.Mutation, UpdateTaxRate.Variables>(UPDATE_TAX_RATE, {
  437. input: {
  438. // Default Channel's defaultTaxZone is Europe (id 2) and the id of the standard TaxRate
  439. // to Europe is 2.
  440. id: 'T_2',
  441. value: 50,
  442. },
  443. });
  444. await awaitRunningJobs(adminClient);
  445. const result = await adminClient.query<SearchGetPrices.Query, SearchGetPrices.Variables>(
  446. SEARCH_GET_PRICES,
  447. {
  448. input: {
  449. groupByProduct: true,
  450. term: 'laptop',
  451. } as SearchInput,
  452. },
  453. );
  454. expect(result.search.items).toEqual([
  455. {
  456. price: { min: 129900, max: 229900 },
  457. priceWithTax: { min: 194850, max: 344850 },
  458. },
  459. ]);
  460. });
  461. it('returns disabled field when not grouped', async () => {
  462. const result = await adminClient.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  463. SEARCH_PRODUCTS,
  464. {
  465. input: {
  466. groupByProduct: false,
  467. take: 3,
  468. },
  469. },
  470. );
  471. expect(result.search.items.map(pick(['productVariantId', 'enabled']))).toEqual([
  472. { productVariantId: 'T_1', enabled: true },
  473. { productVariantId: 'T_2', enabled: true },
  474. { productVariantId: 'T_3', enabled: false },
  475. ]);
  476. });
  477. it('when grouped, disabled is false if at least one variant is enabled', async () => {
  478. await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
  479. UPDATE_PRODUCT_VARIANTS,
  480. {
  481. input: [{ id: 'T_1', enabled: false }, { id: 'T_2', enabled: false }],
  482. },
  483. );
  484. await awaitRunningJobs(adminClient);
  485. const result = await adminClient.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  486. SEARCH_PRODUCTS,
  487. {
  488. input: {
  489. groupByProduct: true,
  490. take: 3,
  491. },
  492. },
  493. );
  494. expect(result.search.items.map(pick(['productId', 'enabled']))).toEqual([
  495. { productId: 'T_1', enabled: true },
  496. { productId: 'T_2', enabled: true },
  497. { productId: 'T_3', enabled: true },
  498. ]);
  499. });
  500. it('when grouped, disabled is true if all variants are disabled', async () => {
  501. await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
  502. UPDATE_PRODUCT_VARIANTS,
  503. {
  504. input: [{ id: 'T_4', enabled: false }],
  505. },
  506. );
  507. await awaitRunningJobs(adminClient);
  508. const result = await adminClient.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  509. SEARCH_PRODUCTS,
  510. {
  511. input: {
  512. groupByProduct: true,
  513. take: 3,
  514. },
  515. },
  516. );
  517. expect(result.search.items.map(pick(['productId', 'enabled']))).toEqual([
  518. { productId: 'T_1', enabled: false },
  519. { productId: 'T_2', enabled: true },
  520. { productId: 'T_3', enabled: true },
  521. ]);
  522. });
  523. it('when grouped, disabled is true product is disabled', async () => {
  524. await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
  525. input: {
  526. id: 'T_3',
  527. enabled: false,
  528. },
  529. });
  530. await awaitRunningJobs(adminClient);
  531. const result = await adminClient.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
  532. SEARCH_PRODUCTS,
  533. {
  534. input: {
  535. groupByProduct: true,
  536. take: 3,
  537. },
  538. },
  539. );
  540. expect(result.search.items.map(pick(['productId', 'enabled']))).toEqual([
  541. { productId: 'T_1', enabled: false },
  542. { productId: 'T_2', enabled: true },
  543. { productId: 'T_3', enabled: false },
  544. ]);
  545. });
  546. });
  547. });
  548. export const SEARCH_PRODUCTS = gql`
  549. query SearchProductsAdmin($input: SearchInput!) {
  550. search(input: $input) {
  551. totalItems
  552. items {
  553. enabled
  554. productId
  555. productName
  556. productPreview
  557. productVariantId
  558. productVariantName
  559. productVariantPreview
  560. sku
  561. }
  562. }
  563. }
  564. `;
  565. export const SEARCH_GET_FACET_VALUES = gql`
  566. query SearchFacetValues($input: SearchInput!) {
  567. search(input: $input) {
  568. totalItems
  569. facetValues {
  570. count
  571. facetValue {
  572. id
  573. name
  574. }
  575. }
  576. }
  577. }
  578. `;
  579. export const SEARCH_GET_PRICES = gql`
  580. query SearchGetPrices($input: SearchInput!) {
  581. search(input: $input) {
  582. items {
  583. price {
  584. ... on PriceRange {
  585. min
  586. max
  587. }
  588. ... on SinglePrice {
  589. value
  590. }
  591. }
  592. priceWithTax {
  593. ... on PriceRange {
  594. min
  595. max
  596. }
  597. ... on SinglePrice {
  598. value
  599. }
  600. }
  601. }
  602. }
  603. }
  604. `;