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

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