e2e-helpers.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. import { Client } from '@elastic/elasticsearch';
  2. import { LogicalOperator, SortOrder } from '@vendure/common/lib/generated-types';
  3. import { SimpleGraphQLClient } from '@vendure/testing';
  4. import { expect } from 'vitest';
  5. import { searchProductsShopDocument } from '../../core/e2e/graphql/shop-definitions';
  6. import { deleteIndices } from '../src/indexing/indexing-utils';
  7. import { searchGetPricesDocument, searchProductsAdminDocument } from './elasticsearch-plugin.e2e-spec';
  8. import { VariablesOf } from './graphql/graphql-admin';
  9. // eslint-disable-next-line @typescript-eslint/no-var-requires
  10. const { elasticsearchHost, elasticsearchPort } = require('./constants');
  11. type SearchInput = VariablesOf<typeof searchProductsAdminDocument>['input'];
  12. export function doAdminSearchQuery(client: SimpleGraphQLClient, input: SearchInput) {
  13. return client.query(searchProductsAdminDocument, { input });
  14. }
  15. export async function testGroupByProduct(client: SimpleGraphQLClient) {
  16. const result = await client.query(searchProductsShopDocument, {
  17. input: {
  18. groupByProduct: true,
  19. },
  20. });
  21. expect(result.search.totalItems).toBe(21);
  22. }
  23. export async function testGroupBySKU(client: SimpleGraphQLClient) {
  24. const result = await client.query(searchProductsShopDocument, {
  25. input: {
  26. term: 'bonsai',
  27. groupBySKU: true,
  28. },
  29. });
  30. expect(result.search.totalItems).toBe(1);
  31. }
  32. export async function testNoGrouping(client: SimpleGraphQLClient) {
  33. const result = await client.query(searchProductsShopDocument, {
  34. input: {
  35. groupByProduct: false,
  36. groupBySKU: false,
  37. },
  38. });
  39. expect(result.search.totalItems).toBe(35);
  40. }
  41. export async function testMatchSearchTerm(client: SimpleGraphQLClient) {
  42. const result = await client.query(searchProductsShopDocument, {
  43. input: {
  44. term: 'camera',
  45. groupByProduct: true,
  46. },
  47. });
  48. expect(result.search.items.map(i => i.productName).sort((a, b) => a.localeCompare(b))).toEqual(
  49. ['Camera Lens', 'Instant Camera', 'SLR Camera'].sort((a, b) => a.localeCompare(b)),
  50. );
  51. }
  52. export async function testMatchFacetIdsAnd(client: SimpleGraphQLClient) {
  53. const result = await client.query(searchProductsShopDocument, {
  54. input: {
  55. facetValueIds: ['T_1', 'T_2'],
  56. facetValueOperator: LogicalOperator.AND,
  57. groupByProduct: true,
  58. sort: {
  59. name: SortOrder.ASC,
  60. },
  61. },
  62. });
  63. expect(result.search.items.map(i => i.productName)).toEqual([
  64. 'Clacky Keyboard',
  65. 'Curvy Monitor',
  66. 'Gaming PC',
  67. 'Hard Drive',
  68. 'Laptop',
  69. 'USB Cable',
  70. ]);
  71. }
  72. export async function testMatchFacetIdsOr(client: SimpleGraphQLClient) {
  73. const result = await client.query(searchProductsShopDocument, {
  74. input: {
  75. facetValueIds: ['T_1', 'T_5'],
  76. facetValueOperator: LogicalOperator.OR,
  77. groupByProduct: true,
  78. sort: {
  79. name: SortOrder.ASC,
  80. },
  81. take: 20,
  82. },
  83. });
  84. expect(result.search.items.map(i => i.productName)).toEqual([
  85. 'Bonsai Tree',
  86. 'Bonsai Tree (Ch2)',
  87. 'Camera Lens',
  88. 'Clacky Keyboard',
  89. 'Curvy Monitor',
  90. 'Gaming PC',
  91. 'Hard Drive',
  92. 'Instant Camera',
  93. 'Laptop',
  94. 'Orchid',
  95. 'SLR Camera',
  96. 'Spiky Cactus',
  97. 'Tripod',
  98. 'USB Cable',
  99. ]);
  100. }
  101. export async function testMatchFacetValueFiltersAnd(client: SimpleGraphQLClient) {
  102. const result = await client.query(searchProductsShopDocument, {
  103. input: {
  104. groupByProduct: true,
  105. facetValueFilters: [{ and: 'T_1' }, { and: 'T_2' }],
  106. },
  107. });
  108. expect(result.search.items.map(i => i.productName).sort((a, b) => a.localeCompare(b))).toEqual(
  109. ['Laptop', 'Curvy Monitor', 'Gaming PC', 'Hard Drive', 'Clacky Keyboard', 'USB Cable'].sort((a, b) =>
  110. a.localeCompare(b),
  111. ),
  112. );
  113. }
  114. export async function testMatchFacetValueFiltersOr(client: SimpleGraphQLClient) {
  115. const result = await client.query(searchProductsShopDocument, {
  116. input: {
  117. groupByProduct: true,
  118. facetValueFilters: [{ or: ['T_1', 'T_5'] }],
  119. sort: {
  120. name: SortOrder.ASC,
  121. },
  122. take: 20,
  123. },
  124. });
  125. expect(result.search.items.map(i => i.productName).sort((a, b) => a.localeCompare(b))).toEqual(
  126. [
  127. 'Bonsai Tree',
  128. 'Bonsai Tree (Ch2)',
  129. 'Camera Lens',
  130. 'Clacky Keyboard',
  131. 'Curvy Monitor',
  132. 'Gaming PC',
  133. 'Hard Drive',
  134. 'Instant Camera',
  135. 'Laptop',
  136. 'Orchid',
  137. 'SLR Camera',
  138. 'Spiky Cactus',
  139. 'Tripod',
  140. 'USB Cable',
  141. ].sort((a, b) => a.localeCompare(b)),
  142. );
  143. }
  144. export async function testMatchFacetValueFiltersOrWithAnd(client: SimpleGraphQLClient) {
  145. const result = await client.query(searchProductsShopDocument, {
  146. input: {
  147. groupByProduct: true,
  148. facetValueFilters: [{ and: 'T_1' }, { or: ['T_2', 'T_3'] }],
  149. },
  150. });
  151. expect(result.search.items.map(i => i.productName).sort((a, b) => a.localeCompare(b))).toEqual(
  152. [
  153. 'Laptop',
  154. 'Curvy Monitor',
  155. 'Gaming PC',
  156. 'Hard Drive',
  157. 'Clacky Keyboard',
  158. 'USB Cable',
  159. 'Instant Camera',
  160. 'Camera Lens',
  161. 'Tripod',
  162. 'SLR Camera',
  163. ].sort((a, b) => a.localeCompare(b)),
  164. );
  165. }
  166. export async function testMatchFacetValueFiltersWithFacetIdsOr(client: SimpleGraphQLClient) {
  167. const result = await client.query(searchProductsShopDocument, {
  168. input: {
  169. facetValueIds: ['T_2', 'T_3'],
  170. facetValueOperator: LogicalOperator.OR,
  171. facetValueFilters: [{ and: 'T_1' }],
  172. groupByProduct: true,
  173. },
  174. });
  175. expect(result.search.items.map(i => i.productName).sort((a, b) => a.localeCompare(b))).toEqual(
  176. [
  177. 'Laptop',
  178. 'Curvy Monitor',
  179. 'Gaming PC',
  180. 'Hard Drive',
  181. 'Clacky Keyboard',
  182. 'USB Cable',
  183. 'Instant Camera',
  184. 'Camera Lens',
  185. 'Tripod',
  186. 'SLR Camera',
  187. ].sort((a, b) => a.localeCompare(b)),
  188. );
  189. }
  190. export async function testMatchFacetValueFiltersWithFacetIdsAnd(client: SimpleGraphQLClient) {
  191. const result = await client.query(searchProductsShopDocument, {
  192. input: {
  193. facetValueIds: ['T_1'],
  194. facetValueFilters: [{ and: 'T_3' }],
  195. facetValueOperator: LogicalOperator.AND,
  196. groupByProduct: true,
  197. },
  198. });
  199. expect(result.search.items.map(i => i.productName).sort((a, b) => a.localeCompare(b))).toEqual(
  200. ['Instant Camera', 'Camera Lens', 'Tripod', 'SLR Camera'].sort((a, b) => a.localeCompare(b)),
  201. );
  202. }
  203. export async function testMatchCollectionId(client: SimpleGraphQLClient) {
  204. const result = await client.query(searchProductsShopDocument, {
  205. input: {
  206. collectionId: 'T_2',
  207. groupByProduct: true,
  208. },
  209. });
  210. expect(result.search.items.map(i => i.productName).sort((a, b) => a.localeCompare(b))).toEqual([
  211. 'Bonsai Tree',
  212. 'Bonsai Tree (Ch2)',
  213. 'Orchid',
  214. 'Spiky Cactus',
  215. ]);
  216. }
  217. export async function testMatchCollectionSlug(client: SimpleGraphQLClient) {
  218. const result = await client.query(searchProductsShopDocument, {
  219. input: {
  220. collectionSlug: 'plants',
  221. groupByProduct: true,
  222. },
  223. });
  224. expect(result.search.items.map(i => i.productName).sort((a, b) => a.localeCompare(b))).toEqual([
  225. 'Bonsai Tree',
  226. 'Bonsai Tree (Ch2)',
  227. 'Orchid',
  228. 'Spiky Cactus',
  229. ]);
  230. }
  231. async function testMatchCollections(client: SimpleGraphQLClient, searchInput: Partial<SearchInput>) {
  232. const result = await client.query(searchProductsShopDocument, {
  233. input: {
  234. groupByProduct: true,
  235. ...searchInput,
  236. sort: { name: SortOrder.ASC },
  237. },
  238. });
  239. // Should return products from both Plants (T_2) and Electronics (T_3) collections
  240. expect(result.search.items.length).toBeGreaterThan(4);
  241. expect(result.search.totalItems).toBeGreaterThan(4);
  242. // Verify that products from both collections are included by checking collectionIds
  243. const allCollectionIds = result.search.items.flatMap(i => i.collectionIds);
  244. // Should contain products from Plants collection (T_2)
  245. expect(allCollectionIds.filter(id => id === 'T_2').length).toBeGreaterThan(0);
  246. // Should contain products from Electronics collection (T_3)
  247. expect(allCollectionIds.filter(id => id === 'T_3').length).toBeGreaterThan(0);
  248. }
  249. export async function testMatchCollectionIds(client: SimpleGraphQLClient) {
  250. return testMatchCollections(client, { collectionIds: ['T_2', 'T_3'] });
  251. }
  252. export async function testMatchCollectionSlugs(client: SimpleGraphQLClient) {
  253. return testMatchCollections(client, { collectionSlugs: ['plants', 'electronics'] });
  254. }
  255. async function testCollectionEdgeCases(
  256. client: SimpleGraphQLClient,
  257. duplicateInput: Partial<SearchInput>,
  258. nonExistentInput: Partial<SearchInput>,
  259. ) {
  260. // Test with duplicates - should handle gracefully
  261. const resultWithDuplicates = await client.query(searchProductsShopDocument, {
  262. input: {
  263. groupByProduct: true,
  264. ...duplicateInput,
  265. },
  266. });
  267. // Should still return Plants collection products, de-duplicated
  268. expect(
  269. resultWithDuplicates.search.items.map(i => i.productName).sort((a, b) => a.localeCompare(b)),
  270. ).toEqual(['Bonsai Tree', 'Bonsai Tree (Ch2)', 'Orchid', 'Spiky Cactus']);
  271. // Test with non-existent collection - should return no results
  272. const resultNonExistent = await client.query(searchProductsShopDocument, {
  273. input: {
  274. groupByProduct: true,
  275. ...nonExistentInput,
  276. },
  277. });
  278. expect(resultNonExistent.search.items).toEqual([]);
  279. expect(resultNonExistent.search.totalItems).toBe(0);
  280. }
  281. export async function testCollectionIdsEdgeCases(client: SimpleGraphQLClient) {
  282. return testCollectionEdgeCases(
  283. client,
  284. { collectionIds: ['T_2', 'T_2', 'T_2'] },
  285. { collectionIds: ['T_999'] },
  286. );
  287. }
  288. export async function testCollectionSlugsEdgeCases(client: SimpleGraphQLClient) {
  289. return testCollectionEdgeCases(
  290. client,
  291. { collectionSlugs: ['plants', 'plants', 'plants'] },
  292. { collectionSlugs: ['non-existent-collection'] },
  293. );
  294. }
  295. export async function testSinglePrices(client: SimpleGraphQLClient) {
  296. const result = await client.query(searchGetPricesDocument, {
  297. input: {
  298. groupByProduct: false,
  299. take: 3,
  300. sort: {
  301. price: SortOrder.ASC,
  302. },
  303. },
  304. });
  305. expect(result.search.items).toEqual([
  306. {
  307. price: { value: 799 },
  308. priceWithTax: { value: 959 },
  309. },
  310. {
  311. price: { value: 1498 },
  312. priceWithTax: { value: 1798 },
  313. },
  314. {
  315. price: { value: 1550 },
  316. priceWithTax: { value: 1860 },
  317. },
  318. ]);
  319. }
  320. export async function testPriceRanges(client: SimpleGraphQLClient) {
  321. const result = await client.query(searchGetPricesDocument, {
  322. input: {
  323. groupByProduct: true,
  324. take: 3,
  325. term: 'laptop',
  326. },
  327. });
  328. expect(result.search.items).toEqual([
  329. {
  330. price: { min: 129900, max: 229900 },
  331. priceWithTax: { min: 155880, max: 275880 },
  332. },
  333. ]);
  334. }
  335. export async function dropElasticIndices(indexPrefix: string) {
  336. const esClient = new Client({
  337. node: `${elasticsearchHost as string}:${elasticsearchPort as string}`,
  338. });
  339. return deleteIndices(esClient, indexPrefix);
  340. }