facet.e2e-spec.ts 34 KB

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