facet.e2e-spec.ts 33 KB

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