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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  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 gql from 'graphql-tag';
  12. import path from 'path';
  13. import {
  14. CREATE_COLLECTION,
  15. UPDATE_COLLECTION,
  16. } from '../../../admin-ui/src/app/data/definitions/collection-definitions';
  17. import { SEARCH_PRODUCTS, UPDATE_PRODUCT } from '../../../admin-ui/src/app/data/definitions/product-definitions';
  18. import { UPDATE_TAX_RATE } from '../../../admin-ui/src/app/data/definitions/settings-definitions';
  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, {
  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, {
  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, {
  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, {
  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, {
  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. });
  186. describe('admin api', () => {
  187. it('group by product', () => testGroupByProduct(adminClient));
  188. it('no grouping', () => testNoGrouping(adminClient));
  189. it('matches search term', () => testMatchSearchTerm(adminClient));
  190. it('matches by facetId', () => testMatchFacetIds(adminClient));
  191. it('matches by collectionId', () => testMatchCollectionId(adminClient));
  192. it('single prices', () => testSinglePrices(shopClient));
  193. it('price ranges', () => testPriceRanges(shopClient));
  194. it('updates index when a Product is changed', async () => {
  195. await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
  196. input: {
  197. id: 'T_1',
  198. facetValueIds: [],
  199. },
  200. });
  201. const result = await adminClient.query<SearchProducts.Query, SearchProducts.Variables>(
  202. SEARCH_PRODUCTS,
  203. {
  204. input: {
  205. facetIds: ['T_2'],
  206. groupByProduct: true,
  207. },
  208. },
  209. );
  210. expect(result.search.items.map(i => i.productName)).toEqual([
  211. 'Curvy Monitor',
  212. 'Gaming PC',
  213. 'Hard Drive',
  214. 'Clacky Keyboard',
  215. 'USB Cable',
  216. ]);
  217. });
  218. it('updates index when a Collection is changed', async () => {
  219. await adminClient.query<UpdateCollection.Mutation, UpdateCollection.Variables>(
  220. UPDATE_COLLECTION,
  221. {
  222. input: {
  223. id: 'T_2',
  224. filters: [
  225. {
  226. code: facetValueCollectionFilter.code,
  227. arguments: [
  228. {
  229. name: 'facetValueIds',
  230. value: `["T_4"]`,
  231. type: ConfigArgType.FACET_VALUE_IDS,
  232. },
  233. ],
  234. },
  235. ],
  236. },
  237. },
  238. );
  239. const result = await adminClient.query<SearchProducts.Query, SearchProducts.Variables>(
  240. SEARCH_PRODUCTS,
  241. {
  242. input: {
  243. collectionId: 'T_2',
  244. groupByProduct: true,
  245. },
  246. },
  247. );
  248. expect(result.search.items.map(i => i.productName)).toEqual([
  249. 'Road Bike',
  250. 'Skipping Rope',
  251. 'Boxing Gloves',
  252. 'Tent',
  253. 'Cruiser Skateboard',
  254. 'Football',
  255. 'Running Shoe',
  256. ]);
  257. });
  258. it('updates index when a Collection created', async () => {
  259. const { createCollection } = await adminClient.query<
  260. CreateCollection.Mutation,
  261. CreateCollection.Variables
  262. >(CREATE_COLLECTION, {
  263. input: {
  264. translations: [
  265. {
  266. languageCode: LanguageCode.en,
  267. name: 'Photo',
  268. description: '',
  269. },
  270. ],
  271. filters: [
  272. {
  273. code: facetValueCollectionFilter.code,
  274. arguments: [
  275. {
  276. name: 'facetValueIds',
  277. value: `["T_3"]`,
  278. type: ConfigArgType.FACET_VALUE_IDS,
  279. },
  280. ],
  281. },
  282. ],
  283. },
  284. });
  285. const result = await adminClient.query<SearchProducts.Query, SearchProducts.Variables>(
  286. SEARCH_PRODUCTS,
  287. {
  288. input: {
  289. collectionId: createCollection.id,
  290. groupByProduct: true,
  291. },
  292. },
  293. );
  294. expect(result.search.items.map(i => i.productName)).toEqual([
  295. 'Instant Camera',
  296. 'Camera Lens',
  297. 'Tripod',
  298. 'SLR Camera',
  299. ]);
  300. });
  301. it('updates index when a taxRate is changed', async () => {
  302. await adminClient.query<UpdateTaxRate.Mutation, UpdateTaxRate.Variables>(UPDATE_TAX_RATE, {
  303. input: {
  304. // Default Channel's defaultTaxZone is Europe (id 2) and the id of the standard TaxRate
  305. // to Europe is 2.
  306. id: 'T_2',
  307. value: 50,
  308. },
  309. });
  310. const result = await adminClient.query(SEARCH_GET_PRICES, {
  311. input: {
  312. groupByProduct: true,
  313. term: 'laptop',
  314. } as SearchInput,
  315. });
  316. expect(result.search.items).toEqual([
  317. {
  318. price: { min: 129900, max: 229900 },
  319. priceWithTax: { min: 194850, max: 344850 },
  320. },
  321. ]);
  322. });
  323. });
  324. });
  325. export const SEARCH_GET_FACET_VALUES = gql`
  326. query SearchProducts($input: SearchInput!) {
  327. search(input: $input) {
  328. totalItems
  329. facetValues {
  330. count
  331. facetValue {
  332. id
  333. name
  334. }
  335. }
  336. }
  337. }
  338. `;
  339. export const SEARCH_GET_PRICES = gql`
  340. query SearchGetPrices($input: SearchInput!) {
  341. search(input: $input) {
  342. items {
  343. price {
  344. ... on PriceRange {
  345. min
  346. max
  347. }
  348. ... on SinglePrice {
  349. value
  350. }
  351. }
  352. priceWithTax {
  353. ... on PriceRange {
  354. min
  355. max
  356. }
  357. ... on SinglePrice {
  358. value
  359. }
  360. }
  361. }
  362. }
  363. }
  364. `;