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

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