1
0

facet.e2e-spec.ts 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059
  1. import { pick } from '@vendure/common/lib/pick';
  2. import { createTestEnvironment, E2E_DEFAULT_CHANNEL_TOKEN } from '@vendure/testing';
  3. import gql from 'graphql-tag';
  4. import path from 'path';
  5. import { afterAll, beforeAll, describe, expect, it } from 'vitest';
  6. import { initialData } from '../../../e2e-common/e2e-initial-data';
  7. import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
  8. import { FACET_VALUE_FRAGMENT } from './graphql/fragments';
  9. import * as Codegen from './graphql/generated-e2e-admin-types';
  10. import {
  11. ChannelFragment,
  12. CurrencyCode,
  13. DeletionResult,
  14. FacetWithValuesFragment,
  15. GetFacetWithValueListDocument,
  16. LanguageCode,
  17. } from './graphql/generated-e2e-admin-types';
  18. import {
  19. ASSIGN_PRODUCT_TO_CHANNEL,
  20. CREATE_CHANNEL,
  21. CREATE_FACET,
  22. CREATE_FACET_VALUE,
  23. GET_FACET_LIST,
  24. GET_FACET_LIST_SIMPLE,
  25. GET_FACET_VALUE,
  26. GET_FACET_VALUES,
  27. GET_FACET_WITH_VALUES,
  28. GET_PRODUCT_WITH_VARIANTS,
  29. UPDATE_FACET,
  30. UPDATE_FACET_VALUE,
  31. UPDATE_PRODUCT,
  32. UPDATE_PRODUCT_VARIANTS,
  33. } from './graphql/shared-definitions';
  34. import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
  35. /* eslint-disable @typescript-eslint/no-non-null-assertion */
  36. describe('Facet resolver', () => {
  37. const { server, adminClient, shopClient } = createTestEnvironment(testConfig());
  38. let brandFacet: FacetWithValuesFragment;
  39. let speakerTypeFacet: FacetWithValuesFragment;
  40. beforeAll(async () => {
  41. await server.init({
  42. initialData,
  43. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
  44. customerCount: 1,
  45. });
  46. await adminClient.asSuperAdmin();
  47. }, TEST_SETUP_TIMEOUT_MS);
  48. afterAll(async () => {
  49. await server.destroy();
  50. });
  51. it('createFacet', async () => {
  52. const result = await adminClient.query<
  53. Codegen.CreateFacetMutation,
  54. Codegen.CreateFacetMutationVariables
  55. >(CREATE_FACET, {
  56. input: {
  57. isPrivate: false,
  58. code: 'speaker-type',
  59. translations: [{ languageCode: LanguageCode.en, name: 'Speaker Type' }],
  60. values: [
  61. {
  62. code: 'portable',
  63. translations: [{ languageCode: LanguageCode.en, name: 'Portable' }],
  64. },
  65. ],
  66. },
  67. });
  68. speakerTypeFacet = result.createFacet;
  69. expect(speakerTypeFacet).toMatchSnapshot();
  70. });
  71. it('updateFacet', async () => {
  72. const result = await adminClient.query<
  73. Codegen.UpdateFacetMutation,
  74. Codegen.UpdateFacetMutationVariables
  75. >(UPDATE_FACET, {
  76. input: {
  77. id: speakerTypeFacet.id,
  78. translations: [{ languageCode: LanguageCode.en, name: 'Speaker Category' }],
  79. isPrivate: true,
  80. },
  81. });
  82. expect(result.updateFacet.name).toBe('Speaker Category');
  83. });
  84. it('createFacetValues', async () => {
  85. const { createFacetValues } = await adminClient.query<
  86. Codegen.CreateFacetValuesMutation,
  87. Codegen.CreateFacetValuesMutationVariables
  88. >(CREATE_FACET_VALUES, {
  89. input: [
  90. {
  91. facetId: speakerTypeFacet.id,
  92. code: 'pc',
  93. translations: [{ languageCode: LanguageCode.en, name: 'PC Speakers' }],
  94. },
  95. {
  96. facetId: speakerTypeFacet.id,
  97. code: 'hi-fi',
  98. translations: [{ languageCode: LanguageCode.en, name: 'Hi Fi Speakers' }],
  99. },
  100. ],
  101. });
  102. expect(createFacetValues.length).toBe(2);
  103. expect(pick(createFacetValues.find(fv => fv.code === 'pc')!, ['code', 'facet', 'name'])).toEqual({
  104. code: 'pc',
  105. facet: {
  106. id: 'T_2',
  107. name: 'Speaker Category',
  108. },
  109. name: 'PC Speakers',
  110. });
  111. expect(pick(createFacetValues.find(fv => fv.code === 'hi-fi')!, ['code', 'facet', 'name'])).toEqual({
  112. code: 'hi-fi',
  113. facet: {
  114. id: 'T_2',
  115. name: 'Speaker Category',
  116. },
  117. name: 'Hi Fi Speakers',
  118. });
  119. });
  120. it('updateFacetValues', async () => {
  121. const portableFacetValue = speakerTypeFacet.values.find(v => v.code === 'portable')!;
  122. const result = await adminClient.query<
  123. Codegen.UpdateFacetValuesMutation,
  124. Codegen.UpdateFacetValuesMutationVariables
  125. >(UPDATE_FACET_VALUES, {
  126. input: [
  127. {
  128. id: portableFacetValue.id,
  129. code: 'compact',
  130. },
  131. ],
  132. });
  133. expect(result.updateFacetValues[0].code).toBe('compact');
  134. });
  135. it('createFacetValue (single)', async () => {
  136. const result = await adminClient.query<
  137. Codegen.CreateFacetValueMutation,
  138. Codegen.CreateFacetValueMutationVariables
  139. >(CREATE_FACET_VALUE, {
  140. input: {
  141. facetId: speakerTypeFacet.id,
  142. code: 'wireless',
  143. translations: [{ languageCode: LanguageCode.en, name: 'Wireless Speakers' }],
  144. },
  145. });
  146. expect(result.createFacetValue).toEqual({
  147. id: expect.any(String),
  148. code: 'wireless',
  149. name: 'Wireless Speakers',
  150. languageCode: 'en',
  151. facet: {
  152. id: speakerTypeFacet.id,
  153. name: 'Speaker Category',
  154. },
  155. translations: [
  156. {
  157. id: expect.any(String),
  158. languageCode: LanguageCode.en,
  159. name: 'Wireless Speakers',
  160. },
  161. ],
  162. });
  163. });
  164. it('updateFacetValue (single)', async () => {
  165. // First get the newly created facet value
  166. const facetWithValues = await adminClient.query<
  167. Codegen.GetFacetWithValuesQuery,
  168. Codegen.GetFacetWithValuesQueryVariables
  169. >(GET_FACET_WITH_VALUES, {
  170. id: speakerTypeFacet.id,
  171. });
  172. const wirelessFacetValue = facetWithValues.facet!.values.find(v => v.code === 'wireless')!;
  173. const result = await adminClient.query<
  174. Codegen.UpdateFacetValueMutation,
  175. Codegen.UpdateFacetValueMutationVariables
  176. >(UPDATE_FACET_VALUE, {
  177. input: {
  178. id: wirelessFacetValue.id,
  179. code: 'bluetooth',
  180. translations: [
  181. {
  182. id: wirelessFacetValue.translations[0].id,
  183. languageCode: LanguageCode.en,
  184. name: 'Bluetooth Speakers',
  185. },
  186. ],
  187. },
  188. });
  189. expect(result.updateFacetValue).toEqual({
  190. id: wirelessFacetValue.id,
  191. code: 'bluetooth',
  192. name: 'Bluetooth Speakers',
  193. languageCode: 'en',
  194. facet: {
  195. id: speakerTypeFacet.id,
  196. name: 'Speaker Category',
  197. },
  198. translations: [
  199. {
  200. id: expect.any(String),
  201. languageCode: LanguageCode.en,
  202. name: 'Bluetooth Speakers',
  203. },
  204. ],
  205. });
  206. });
  207. it('facets', async () => {
  208. const result = await adminClient.query<Codegen.GetFacetListQuery>(GET_FACET_LIST);
  209. const { items } = result.facets;
  210. expect(items.length).toBe(2);
  211. expect(items[0].name).toBe('category');
  212. expect(items[1].name).toBe('Speaker Category');
  213. brandFacet = items[0];
  214. speakerTypeFacet = items[1];
  215. });
  216. it('facets by shop-api', async () => {
  217. const result = await shopClient.query<Codegen.GetFacetListQuery>(GET_FACET_LIST_SIMPLE);
  218. const { items } = result.facets;
  219. expect(items.length).toBe(1);
  220. expect(items[0].name).toBe('category');
  221. });
  222. it('facet', async () => {
  223. const result = await adminClient.query<
  224. Codegen.GetFacetWithValuesQuery,
  225. Codegen.GetFacetWithValuesQueryVariables
  226. >(GET_FACET_WITH_VALUES, {
  227. id: speakerTypeFacet.id,
  228. });
  229. expect(result.facet!.name).toBe('Speaker Category');
  230. });
  231. it('facet with valueList', async () => {
  232. const result = await adminClient.query(GetFacetWithValueListDocument, {
  233. id: speakerTypeFacet.id,
  234. });
  235. expect(result.facet?.valueList.totalItems).toBe(4);
  236. });
  237. it('facet with valueList with name filter', async () => {
  238. const result = await adminClient.query(GetFacetWithValueListDocument, {
  239. id: speakerTypeFacet.id,
  240. options: {
  241. filter: {
  242. name: {
  243. contains: 'spea',
  244. },
  245. },
  246. },
  247. });
  248. expect(result.facet?.valueList.totalItems).toBe(3);
  249. });
  250. it('facetValues list query', async () => {
  251. const result = await adminClient.query<
  252. Codegen.GetFacetValuesQuery,
  253. Codegen.GetFacetValuesQueryVariables
  254. >(GET_FACET_VALUES, {
  255. options: {
  256. filter: {
  257. facetId: { eq: speakerTypeFacet.id },
  258. },
  259. },
  260. });
  261. expect(result.facetValues.totalItems).toBe(4);
  262. expect(result.facetValues.items.length).toBe(4);
  263. expect(result.facetValues.items.map(v => v.code).sort()).toEqual([
  264. 'bluetooth',
  265. 'compact',
  266. 'hi-fi',
  267. 'pc',
  268. ]);
  269. expect(result.facetValues.items.every(v => v.facet.id === speakerTypeFacet.id)).toBe(true);
  270. });
  271. it('facetValue single query', async () => {
  272. const pcFacetValue = speakerTypeFacet.values.find(v => v.code === 'pc')!;
  273. const result = await adminClient.query<
  274. Codegen.GetFacetValueQuery,
  275. Codegen.GetFacetValueQueryVariables
  276. >(GET_FACET_VALUE, {
  277. id: pcFacetValue.id,
  278. });
  279. expect(result.facetValue).toBeDefined();
  280. expect(result.facetValue!.id).toBe(pcFacetValue.id);
  281. expect(result.facetValue!.code).toBe('pc');
  282. expect(result.facetValue!.name).toBe('PC Speakers');
  283. expect(result.facetValue!.facet.id).toBe(speakerTypeFacet.id);
  284. expect(result.facetValue!.facet.name).toBe('Speaker Category');
  285. });
  286. it('product.facetValues resolver omits private facets in shop-api', async () => {
  287. const publicFacetValue = brandFacet.values[0];
  288. const privateFacetValue = speakerTypeFacet.values[0];
  289. await adminClient.query<Codegen.UpdateProductMutation, Codegen.UpdateProductMutationVariables>(
  290. UPDATE_PRODUCT,
  291. {
  292. input: {
  293. id: 'T_1',
  294. facetValueIds: [publicFacetValue.id, privateFacetValue.id],
  295. },
  296. },
  297. );
  298. const { product } = await shopClient.query<
  299. Codegen.GetProductWithFacetValuesQuery,
  300. Codegen.GetProductWithFacetValuesQueryVariables
  301. >(GET_PRODUCT_WITH_FACET_VALUES, {
  302. id: 'T_1',
  303. });
  304. expect(product?.facetValues.map(v => v.id).includes(publicFacetValue.id)).toBe(true);
  305. expect(product?.facetValues.map(v => v.id).includes(privateFacetValue.id)).toBe(false);
  306. });
  307. it('productVariant.facetValues resolver omits private facets in shop-api', async () => {
  308. const publicFacetValue = brandFacet.values[0];
  309. const privateFacetValue = speakerTypeFacet.values[0];
  310. await adminClient.query<
  311. Codegen.UpdateProductVariantsMutation,
  312. Codegen.UpdateProductVariantsMutationVariables
  313. >(UPDATE_PRODUCT_VARIANTS, {
  314. input: [
  315. {
  316. id: 'T_1',
  317. facetValueIds: [publicFacetValue.id, privateFacetValue.id],
  318. },
  319. ],
  320. });
  321. const { product } = await shopClient.query<
  322. Codegen.GetProductWithFacetValuesQuery,
  323. Codegen.GetProductWithFacetValuesQueryVariables
  324. >(GET_PRODUCT_WITH_FACET_VALUES, {
  325. id: 'T_1',
  326. });
  327. const productVariant1 = product?.variants.find(v => v.id === 'T_1');
  328. expect(productVariant1?.facetValues.map(v => v.id).includes(publicFacetValue.id)).toBe(true);
  329. expect(productVariant1?.facetValues.map(v => v.id).includes(privateFacetValue.id)).toBe(false);
  330. });
  331. describe('deletion', () => {
  332. let products: Codegen.GetProductListWithVariantsQuery['products']['items'];
  333. beforeAll(async () => {
  334. // add the FacetValues to products and variants
  335. const result1 = await adminClient.query<Codegen.GetProductListWithVariantsQuery>(
  336. GET_PRODUCTS_LIST_WITH_VARIANTS,
  337. );
  338. products = result1.products.items;
  339. const pcFacetValue = speakerTypeFacet.values.find(v => v.code === 'pc')!;
  340. const hifiFacetValue = speakerTypeFacet.values.find(v => v.code === 'hi-fi')!;
  341. await adminClient.query<Codegen.UpdateProductMutation, Codegen.UpdateProductMutationVariables>(
  342. UPDATE_PRODUCT,
  343. {
  344. input: {
  345. id: products[0].id,
  346. facetValueIds: [pcFacetValue.id],
  347. },
  348. },
  349. );
  350. await adminClient.query<
  351. Codegen.UpdateProductVariantsMutation,
  352. Codegen.UpdateProductVariantsMutationVariables
  353. >(UPDATE_PRODUCT_VARIANTS, {
  354. input: [
  355. {
  356. id: products[0].variants[0].id,
  357. facetValueIds: [pcFacetValue.id],
  358. },
  359. ],
  360. });
  361. await adminClient.query<Codegen.UpdateProductMutation, Codegen.UpdateProductMutationVariables>(
  362. UPDATE_PRODUCT,
  363. {
  364. input: {
  365. id: products[1].id,
  366. facetValueIds: [hifiFacetValue.id],
  367. },
  368. },
  369. );
  370. });
  371. it('deleteFacetValues deletes unused facetValue', async () => {
  372. const facetValueToDelete = speakerTypeFacet.values.find(v => v.code === 'compact')!;
  373. const result1 = await adminClient.query<
  374. Codegen.DeleteFacetValuesMutation,
  375. Codegen.DeleteFacetValuesMutationVariables
  376. >(DELETE_FACET_VALUES, {
  377. ids: [facetValueToDelete.id],
  378. force: false,
  379. });
  380. const result2 = await adminClient.query<
  381. Codegen.GetFacetWithValuesQuery,
  382. Codegen.GetFacetWithValuesQueryVariables
  383. >(GET_FACET_WITH_VALUES, {
  384. id: speakerTypeFacet.id,
  385. });
  386. expect(result1.deleteFacetValues).toEqual([
  387. {
  388. result: DeletionResult.DELETED,
  389. message: '',
  390. },
  391. ]);
  392. expect(result2.facet!.values[0]).not.toEqual(facetValueToDelete);
  393. });
  394. it('deleteFacetValues for FacetValue in use returns NOT_DELETED', async () => {
  395. const facetValueToDelete = speakerTypeFacet.values.find(v => v.code === 'pc')!;
  396. const result1 = await adminClient.query<
  397. Codegen.DeleteFacetValuesMutation,
  398. Codegen.DeleteFacetValuesMutationVariables
  399. >(DELETE_FACET_VALUES, {
  400. ids: [facetValueToDelete.id],
  401. force: false,
  402. });
  403. const result2 = await adminClient.query<
  404. Codegen.GetFacetWithValuesQuery,
  405. Codegen.GetFacetWithValuesQueryVariables
  406. >(GET_FACET_WITH_VALUES, {
  407. id: speakerTypeFacet.id,
  408. });
  409. expect(result1.deleteFacetValues).toEqual([
  410. {
  411. result: DeletionResult.NOT_DELETED,
  412. message: 'The FacetValue "pc" is assigned to 1 Product, 1 ProductVariant',
  413. },
  414. ]);
  415. expect(result2.facet!.values.find(v => v.id === facetValueToDelete.id)).toBeDefined();
  416. });
  417. it('deleteFacetValues for FacetValue in use can be force deleted', async () => {
  418. const facetValueToDelete = speakerTypeFacet.values.find(v => v.code === 'pc')!;
  419. const result1 = await adminClient.query<
  420. Codegen.DeleteFacetValuesMutation,
  421. Codegen.DeleteFacetValuesMutationVariables
  422. >(DELETE_FACET_VALUES, {
  423. ids: [facetValueToDelete.id],
  424. force: true,
  425. });
  426. expect(result1.deleteFacetValues).toEqual([
  427. {
  428. result: DeletionResult.DELETED,
  429. message:
  430. 'The selected FacetValue was removed from 1 Product, 1 ProductVariant and deleted',
  431. },
  432. ]);
  433. // FacetValue no longer in the Facet.values array
  434. const result2 = await adminClient.query<
  435. Codegen.GetFacetWithValuesQuery,
  436. Codegen.GetFacetWithValuesQueryVariables
  437. >(GET_FACET_WITH_VALUES, {
  438. id: speakerTypeFacet.id,
  439. });
  440. expect(result2.facet!.values[0]).not.toEqual(facetValueToDelete);
  441. // FacetValue no longer in the Product.facetValues array
  442. const result3 = await adminClient.query<
  443. Codegen.GetProductWithVariantsQuery,
  444. Codegen.GetProductWithVariantsQueryVariables
  445. >(GET_PRODUCT_WITH_VARIANTS, {
  446. id: products[0].id,
  447. });
  448. expect(result3.product!.facetValues).toEqual([]);
  449. });
  450. it('deleteFacet that is in use returns NOT_DELETED', async () => {
  451. const result1 = await adminClient.query<
  452. Codegen.DeleteFacetMutation,
  453. Codegen.DeleteFacetMutationVariables
  454. >(DELETE_FACET, {
  455. id: speakerTypeFacet.id,
  456. force: false,
  457. });
  458. const result2 = await adminClient.query<
  459. Codegen.GetFacetWithValuesQuery,
  460. Codegen.GetFacetWithValuesQueryVariables
  461. >(GET_FACET_WITH_VALUES, {
  462. id: speakerTypeFacet.id,
  463. });
  464. expect(result1.deleteFacet).toEqual({
  465. result: DeletionResult.NOT_DELETED,
  466. message: 'The Facet "speaker-type" includes FacetValues which are assigned to 1 Product',
  467. });
  468. expect(result2.facet).not.toBe(null);
  469. });
  470. it('deleteFacet that is in use can be force deleted', async () => {
  471. const result1 = await adminClient.query<
  472. Codegen.DeleteFacetMutation,
  473. Codegen.DeleteFacetMutationVariables
  474. >(DELETE_FACET, {
  475. id: speakerTypeFacet.id,
  476. force: true,
  477. });
  478. expect(result1.deleteFacet).toEqual({
  479. result: DeletionResult.DELETED,
  480. message: 'The Facet was deleted and its FacetValues were removed from 1 Product',
  481. });
  482. // FacetValue no longer in the Facet.values array
  483. const result2 = await adminClient.query<
  484. Codegen.GetFacetWithValuesQuery,
  485. Codegen.GetFacetWithValuesQueryVariables
  486. >(GET_FACET_WITH_VALUES, {
  487. id: speakerTypeFacet.id,
  488. });
  489. expect(result2.facet).toBe(null);
  490. // FacetValue no longer in the Product.facetValues array
  491. const result3 = await adminClient.query<
  492. Codegen.GetProductWithVariantsQuery,
  493. Codegen.GetProductWithVariantsQueryVariables
  494. >(GET_PRODUCT_WITH_VARIANTS, {
  495. id: products[1].id,
  496. });
  497. expect(result3.product!.facetValues).toEqual([]);
  498. });
  499. it('deleteFacet with no FacetValues works', async () => {
  500. const { createFacet } = await adminClient.query<
  501. Codegen.CreateFacetMutation,
  502. Codegen.CreateFacetMutationVariables
  503. >(CREATE_FACET, {
  504. input: {
  505. code: 'test',
  506. isPrivate: false,
  507. translations: [{ languageCode: LanguageCode.en, name: 'Test' }],
  508. },
  509. });
  510. const result = await adminClient.query<
  511. Codegen.DeleteFacetMutation,
  512. Codegen.DeleteFacetMutationVariables
  513. >(DELETE_FACET, {
  514. id: createFacet.id,
  515. force: false,
  516. });
  517. expect(result.deleteFacet.result).toBe(DeletionResult.DELETED);
  518. });
  519. });
  520. describe('channels', () => {
  521. const SECOND_CHANNEL_TOKEN = 'second_channel_token';
  522. let secondChannel: ChannelFragment;
  523. let createdFacet: Codegen.CreateFacetMutation['createFacet'];
  524. beforeAll(async () => {
  525. const { createChannel } = await adminClient.query<
  526. Codegen.CreateChannelMutation,
  527. Codegen.CreateChannelMutationVariables
  528. >(CREATE_CHANNEL, {
  529. input: {
  530. code: 'second-channel',
  531. token: SECOND_CHANNEL_TOKEN,
  532. defaultLanguageCode: LanguageCode.en,
  533. currencyCode: CurrencyCode.USD,
  534. pricesIncludeTax: true,
  535. defaultShippingZoneId: 'T_1',
  536. defaultTaxZoneId: 'T_1',
  537. },
  538. });
  539. secondChannel = createChannel as ChannelFragment;
  540. const { assignProductsToChannel } = await adminClient.query<
  541. Codegen.AssignProductsToChannelMutation,
  542. Codegen.AssignProductsToChannelMutationVariables
  543. >(ASSIGN_PRODUCT_TO_CHANNEL, {
  544. input: {
  545. channelId: secondChannel.id,
  546. productIds: ['T_1'],
  547. priceFactor: 0.5,
  548. },
  549. });
  550. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  551. });
  552. it('create Facet in channel', async () => {
  553. const { createFacet } = await adminClient.query<
  554. Codegen.CreateFacetMutation,
  555. Codegen.CreateFacetMutationVariables
  556. >(CREATE_FACET, {
  557. input: {
  558. isPrivate: false,
  559. code: 'channel-facet',
  560. translations: [{ languageCode: LanguageCode.en, name: 'Channel Facet' }],
  561. values: [
  562. {
  563. code: 'channel-value-1',
  564. translations: [{ languageCode: LanguageCode.en, name: 'Channel Value 1' }],
  565. },
  566. {
  567. code: 'channel-value-2',
  568. translations: [{ languageCode: LanguageCode.en, name: 'Channel Value 2' }],
  569. },
  570. ],
  571. },
  572. });
  573. expect(createFacet.code).toBe('channel-facet');
  574. createdFacet = createFacet;
  575. });
  576. it('facets list in channel', async () => {
  577. const result = await adminClient.query<Codegen.GetFacetListQuery>(GET_FACET_LIST);
  578. const { items } = result.facets;
  579. expect(items.length).toBe(1);
  580. expect(items.map(i => i.code)).toEqual(['channel-facet']);
  581. });
  582. it('Product.facetValues in channel', async () => {
  583. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  584. await adminClient.query<Codegen.UpdateProductMutation, Codegen.UpdateProductMutationVariables>(
  585. UPDATE_PRODUCT,
  586. {
  587. input: {
  588. id: 'T_1',
  589. facetValueIds: [brandFacet.values[0].id, ...createdFacet.values.map(v => v.id)],
  590. },
  591. },
  592. );
  593. await adminClient.query<
  594. Codegen.UpdateProductVariantsMutation,
  595. Codegen.UpdateProductVariantsMutationVariables
  596. >(UPDATE_PRODUCT_VARIANTS, {
  597. input: [
  598. {
  599. id: 'T_1',
  600. facetValueIds: [brandFacet.values[0].id, ...createdFacet.values.map(v => v.id)],
  601. },
  602. ],
  603. });
  604. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  605. const { product } = await adminClient.query<
  606. Codegen.GetProductWithVariantsQuery,
  607. Codegen.GetProductWithVariantsQueryVariables
  608. >(GET_PRODUCT_WITH_VARIANTS, {
  609. id: 'T_1',
  610. });
  611. expect(product?.facetValues.map(fv => fv.code).sort()).toEqual([
  612. 'channel-value-1',
  613. 'channel-value-2',
  614. ]);
  615. });
  616. it('ProductVariant.facetValues in channel', async () => {
  617. const { product } = await adminClient.query<
  618. Codegen.GetProductWithVariantsQuery,
  619. Codegen.GetProductWithVariantsQueryVariables
  620. >(GET_PRODUCT_WITH_VARIANTS, {
  621. id: 'T_1',
  622. });
  623. expect(product?.variants[0].facetValues.map(fv => fv.code).sort()).toEqual([
  624. 'channel-value-1',
  625. 'channel-value-2',
  626. ]);
  627. });
  628. it('updating Product facetValuesIds in channel only affects that channel', async () => {
  629. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  630. await adminClient.query<Codegen.UpdateProductMutation, Codegen.UpdateProductMutationVariables>(
  631. UPDATE_PRODUCT,
  632. {
  633. input: {
  634. id: 'T_1',
  635. facetValueIds: [createdFacet.values[0].id],
  636. },
  637. },
  638. );
  639. const { product: productC2 } = await adminClient.query<
  640. Codegen.GetProductWithVariantsQuery,
  641. Codegen.GetProductWithVariantsQueryVariables
  642. >(GET_PRODUCT_WITH_VARIANTS, {
  643. id: 'T_1',
  644. });
  645. expect(productC2?.facetValues.map(fv => fv.code)).toEqual([createdFacet.values[0].code]);
  646. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  647. const { product: productCD } = await adminClient.query<
  648. Codegen.GetProductWithVariantsQuery,
  649. Codegen.GetProductWithVariantsQueryVariables
  650. >(GET_PRODUCT_WITH_VARIANTS, {
  651. id: 'T_1',
  652. });
  653. expect(productCD?.facetValues.map(fv => fv.code)).toEqual([
  654. brandFacet.values[0].code,
  655. createdFacet.values[0].code,
  656. ]);
  657. });
  658. it('updating ProductVariant facetValuesIds in channel only affects that channel', async () => {
  659. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  660. await adminClient.query<
  661. Codegen.UpdateProductVariantsMutation,
  662. Codegen.UpdateProductVariantsMutationVariables
  663. >(UPDATE_PRODUCT_VARIANTS, {
  664. input: [
  665. {
  666. id: 'T_1',
  667. facetValueIds: [createdFacet.values[0].id],
  668. },
  669. ],
  670. });
  671. const { product: productC2 } = await adminClient.query<
  672. Codegen.GetProductWithVariantsQuery,
  673. Codegen.GetProductWithVariantsQueryVariables
  674. >(GET_PRODUCT_WITH_VARIANTS, {
  675. id: 'T_1',
  676. });
  677. expect(productC2?.variants.find(v => v.id === 'T_1')?.facetValues.map(fv => fv.code)).toEqual([
  678. createdFacet.values[0].code,
  679. ]);
  680. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  681. const { product: productCD } = await adminClient.query<
  682. Codegen.GetProductWithVariantsQuery,
  683. Codegen.GetProductWithVariantsQueryVariables
  684. >(GET_PRODUCT_WITH_VARIANTS, {
  685. id: 'T_1',
  686. });
  687. expect(productCD?.variants.find(v => v.id === 'T_1')?.facetValues.map(fv => fv.code)).toEqual([
  688. brandFacet.values[0].code,
  689. createdFacet.values[0].code,
  690. ]);
  691. });
  692. it(
  693. 'attempting to create FacetValue in Facet from another Channel throws',
  694. assertThrowsWithMessage(async () => {
  695. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  696. await adminClient.query<
  697. Codegen.CreateFacetValuesMutation,
  698. Codegen.CreateFacetValuesMutationVariables
  699. >(CREATE_FACET_VALUES, {
  700. input: [
  701. {
  702. facetId: brandFacet.id,
  703. code: 'channel-brand',
  704. translations: [{ languageCode: LanguageCode.en, name: 'Channel Brand' }],
  705. },
  706. ],
  707. });
  708. }, 'No Facet with the id "1" could be found'),
  709. );
  710. it('removing from channel with error', async () => {
  711. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  712. const { facets: before } =
  713. await adminClient.query<Codegen.GetFacetListSimpleQuery>(GET_FACET_LIST_SIMPLE);
  714. expect(before.items).toEqual([{ id: 'T_4', name: 'Channel Facet' }]);
  715. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  716. const { removeFacetsFromChannel } = await adminClient.query<
  717. Codegen.RemoveFacetsFromChannelMutation,
  718. Codegen.RemoveFacetsFromChannelMutationVariables
  719. >(REMOVE_FACETS_FROM_CHANNEL, {
  720. input: {
  721. channelId: secondChannel.id,
  722. facetIds: [createdFacet.id],
  723. force: false,
  724. },
  725. });
  726. expect(removeFacetsFromChannel).toEqual([
  727. {
  728. errorCode: 'FACET_IN_USE_ERROR',
  729. message:
  730. 'The Facet "channel-facet" includes FacetValues which are assigned to 1 Product 1 ProductVariant',
  731. productCount: 1,
  732. variantCount: 1,
  733. },
  734. ]);
  735. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  736. const { facets: after } =
  737. await adminClient.query<Codegen.GetFacetListSimpleQuery>(GET_FACET_LIST_SIMPLE);
  738. expect(after.items).toEqual([{ id: 'T_4', name: 'Channel Facet' }]);
  739. });
  740. it('force removing from channel', async () => {
  741. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  742. const { facets: before } =
  743. await adminClient.query<Codegen.GetFacetListSimpleQuery>(GET_FACET_LIST_SIMPLE);
  744. expect(before.items).toEqual([{ id: 'T_4', name: 'Channel Facet' }]);
  745. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  746. const { removeFacetsFromChannel } = await adminClient.query<
  747. Codegen.RemoveFacetsFromChannelMutation,
  748. Codegen.RemoveFacetsFromChannelMutationVariables
  749. >(REMOVE_FACETS_FROM_CHANNEL, {
  750. input: {
  751. channelId: secondChannel.id,
  752. facetIds: [createdFacet.id],
  753. force: true,
  754. },
  755. });
  756. expect(removeFacetsFromChannel).toEqual([{ id: 'T_4', name: 'Channel Facet' }]);
  757. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  758. const { facets: after } =
  759. await adminClient.query<Codegen.GetFacetListSimpleQuery>(GET_FACET_LIST_SIMPLE);
  760. expect(after.items).toEqual([]);
  761. });
  762. it('assigning to channel', async () => {
  763. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  764. const { facets: before } =
  765. await adminClient.query<Codegen.GetFacetListSimpleQuery>(GET_FACET_LIST_SIMPLE);
  766. expect(before.items).toEqual([]);
  767. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  768. const { assignFacetsToChannel } = await adminClient.query<
  769. Codegen.AssignFacetsToChannelMutation,
  770. Codegen.AssignFacetsToChannelMutationVariables
  771. >(ASSIGN_FACETS_TO_CHANNEL, {
  772. input: {
  773. channelId: secondChannel.id,
  774. facetIds: [createdFacet.id],
  775. },
  776. });
  777. expect(assignFacetsToChannel).toEqual([{ id: 'T_4', name: 'Channel Facet' }]);
  778. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  779. const { facets: after } =
  780. await adminClient.query<Codegen.GetFacetListSimpleQuery>(GET_FACET_LIST_SIMPLE);
  781. expect(after.items).toEqual([{ id: 'T_4', name: 'Channel Facet' }]);
  782. });
  783. });
  784. // https://github.com/vendurehq/vendure/issues/715
  785. describe('code conflicts', () => {
  786. function createFacetWithCode(code: string) {
  787. return adminClient.query<Codegen.CreateFacetMutation, Codegen.CreateFacetMutationVariables>(
  788. CREATE_FACET,
  789. {
  790. input: {
  791. isPrivate: false,
  792. code,
  793. translations: [{ languageCode: LanguageCode.en, name: `Test Facet (${code})` }],
  794. values: [],
  795. },
  796. },
  797. );
  798. }
  799. // https://github.com/vendurehq/vendure/issues/831
  800. it('updateFacet with unchanged code', async () => {
  801. const { createFacet } = await createFacetWithCode('some-new-facet');
  802. const result = await adminClient.query<
  803. Codegen.UpdateFacetMutation,
  804. Codegen.UpdateFacetMutationVariables
  805. >(UPDATE_FACET, {
  806. input: {
  807. id: createFacet.id,
  808. code: createFacet.code,
  809. },
  810. });
  811. expect(result.updateFacet.code).toBe(createFacet.code);
  812. });
  813. it('createFacet with conflicting slug gets renamed', async () => {
  814. const { createFacet: result1 } = await createFacetWithCode('test');
  815. expect(result1.code).toBe('test');
  816. const { createFacet: result2 } = await createFacetWithCode('test');
  817. expect(result2.code).toBe('test-2');
  818. });
  819. it('updateFacet with conflicting slug gets renamed', async () => {
  820. const { createFacet } = await createFacetWithCode('foo');
  821. expect(createFacet.code).toBe('foo');
  822. const { updateFacet } = await adminClient.query<
  823. Codegen.UpdateFacetMutation,
  824. Codegen.UpdateFacetMutationVariables
  825. >(UPDATE_FACET, {
  826. input: {
  827. id: createFacet.id,
  828. code: 'test-2',
  829. },
  830. });
  831. expect(updateFacet.code).toBe('test-3');
  832. });
  833. });
  834. });
  835. export const GET_FACET_WITH_VALUE_LIST = gql`
  836. query GetFacetWithValueList($id: ID!, $options: FacetValueListOptions) {
  837. facet(id: $id) {
  838. id
  839. languageCode
  840. isPrivate
  841. code
  842. name
  843. valueList(options: $options) {
  844. items {
  845. ...FacetValue
  846. }
  847. totalItems
  848. }
  849. }
  850. }
  851. ${FACET_VALUE_FRAGMENT}
  852. `;
  853. const DELETE_FACET_VALUES = gql`
  854. mutation DeleteFacetValues($ids: [ID!]!, $force: Boolean) {
  855. deleteFacetValues(ids: $ids, force: $force) {
  856. result
  857. message
  858. }
  859. }
  860. `;
  861. const DELETE_FACET = gql`
  862. mutation DeleteFacet($id: ID!, $force: Boolean) {
  863. deleteFacet(id: $id, force: $force) {
  864. result
  865. message
  866. }
  867. }
  868. `;
  869. const GET_PRODUCT_WITH_FACET_VALUES = gql`
  870. query GetProductWithFacetValues($id: ID!) {
  871. product(id: $id) {
  872. id
  873. facetValues {
  874. id
  875. name
  876. code
  877. }
  878. variants {
  879. id
  880. facetValues {
  881. id
  882. name
  883. code
  884. }
  885. }
  886. }
  887. }
  888. `;
  889. const GET_PRODUCTS_LIST_WITH_VARIANTS = gql`
  890. query GetProductListWithVariants {
  891. products {
  892. items {
  893. id
  894. name
  895. variants {
  896. id
  897. name
  898. }
  899. }
  900. totalItems
  901. }
  902. }
  903. `;
  904. export const CREATE_FACET_VALUES = gql`
  905. mutation CreateFacetValues($input: [CreateFacetValueInput!]!) {
  906. createFacetValues(input: $input) {
  907. ...FacetValue
  908. }
  909. }
  910. ${FACET_VALUE_FRAGMENT}
  911. `;
  912. export const UPDATE_FACET_VALUES = gql`
  913. mutation UpdateFacetValues($input: [UpdateFacetValueInput!]!) {
  914. updateFacetValues(input: $input) {
  915. ...FacetValue
  916. }
  917. }
  918. ${FACET_VALUE_FRAGMENT}
  919. `;
  920. export const ASSIGN_FACETS_TO_CHANNEL = gql`
  921. mutation AssignFacetsToChannel($input: AssignFacetsToChannelInput!) {
  922. assignFacetsToChannel(input: $input) {
  923. id
  924. name
  925. }
  926. }
  927. `;
  928. export const REMOVE_FACETS_FROM_CHANNEL = gql`
  929. mutation RemoveFacetsFromChannel($input: RemoveFacetsFromChannelInput!) {
  930. removeFacetsFromChannel(input: $input) {
  931. ... on Facet {
  932. id
  933. name
  934. }
  935. ... on FacetInUseError {
  936. errorCode
  937. message
  938. productCount
  939. variantCount
  940. }
  941. }
  942. }
  943. `;