facet.e2e-spec.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642
  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 { initialData } from '../../../e2e-common/e2e-initial-data';
  6. import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
  7. import { FACET_VALUE_FRAGMENT, FACET_WITH_VALUES_FRAGMENT } from './graphql/fragments';
  8. import {
  9. AssignProductsToChannel,
  10. ChannelFragment,
  11. CreateChannel,
  12. CreateFacet,
  13. CreateFacetValues,
  14. CurrencyCode,
  15. DeleteFacet,
  16. DeleteFacetValues,
  17. DeletionResult,
  18. FacetWithValues,
  19. GetFacetList,
  20. GetFacetWithValues,
  21. GetProductListWithVariants,
  22. GetProductWithVariants,
  23. LanguageCode,
  24. UpdateFacet,
  25. UpdateFacetValues,
  26. UpdateProduct,
  27. UpdateProductVariants,
  28. } from './graphql/generated-e2e-admin-types';
  29. import {
  30. ASSIGN_PRODUCT_TO_CHANNEL,
  31. CREATE_CHANNEL,
  32. CREATE_FACET,
  33. GET_FACET_LIST,
  34. GET_PRODUCT_WITH_VARIANTS,
  35. UPDATE_FACET,
  36. UPDATE_PRODUCT,
  37. UPDATE_PRODUCT_VARIANTS,
  38. } from './graphql/shared-definitions';
  39. import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
  40. // tslint:disable:no-non-null-assertion
  41. describe('Facet resolver', () => {
  42. const { server, adminClient } = createTestEnvironment(testConfig);
  43. let brandFacet: FacetWithValues.Fragment;
  44. let speakerTypeFacet: FacetWithValues.Fragment;
  45. beforeAll(async () => {
  46. await server.init({
  47. initialData,
  48. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
  49. customerCount: 1,
  50. });
  51. await adminClient.asSuperAdmin();
  52. }, TEST_SETUP_TIMEOUT_MS);
  53. afterAll(async () => {
  54. await server.destroy();
  55. });
  56. it('createFacet', async () => {
  57. const result = await adminClient.query<CreateFacet.Mutation, CreateFacet.Variables>(CREATE_FACET, {
  58. input: {
  59. isPrivate: false,
  60. code: 'speaker-type',
  61. translations: [{ languageCode: LanguageCode.en, name: 'Speaker Type' }],
  62. values: [
  63. {
  64. code: 'portable',
  65. translations: [{ languageCode: LanguageCode.en, name: 'Portable' }],
  66. },
  67. ],
  68. },
  69. });
  70. speakerTypeFacet = result.createFacet;
  71. expect(speakerTypeFacet).toMatchSnapshot();
  72. });
  73. it('updateFacet', async () => {
  74. const result = await adminClient.query<UpdateFacet.Mutation, UpdateFacet.Variables>(UPDATE_FACET, {
  75. input: {
  76. id: speakerTypeFacet.id,
  77. translations: [{ languageCode: LanguageCode.en, name: 'Speaker Category' }],
  78. },
  79. });
  80. expect(result.updateFacet.name).toBe('Speaker Category');
  81. });
  82. it('createFacetValues', async () => {
  83. const { createFacetValues } = await adminClient.query<
  84. CreateFacetValues.Mutation,
  85. CreateFacetValues.Variables
  86. >(CREATE_FACET_VALUES, {
  87. input: [
  88. {
  89. facetId: speakerTypeFacet.id,
  90. code: 'pc',
  91. translations: [{ languageCode: LanguageCode.en, name: 'PC Speakers' }],
  92. },
  93. {
  94. facetId: speakerTypeFacet.id,
  95. code: 'hi-fi',
  96. translations: [{ languageCode: LanguageCode.en, name: 'Hi Fi Speakers' }],
  97. },
  98. ],
  99. });
  100. expect(createFacetValues.length).toBe(2);
  101. expect(pick(createFacetValues.find(fv => fv.code === 'pc')!, ['code', 'facet', 'name'])).toEqual({
  102. code: 'pc',
  103. facet: {
  104. id: 'T_2',
  105. name: 'Speaker Category',
  106. },
  107. name: 'PC Speakers',
  108. });
  109. expect(pick(createFacetValues.find(fv => fv.code === 'hi-fi')!, ['code', 'facet', 'name'])).toEqual({
  110. code: 'hi-fi',
  111. facet: {
  112. id: 'T_2',
  113. name: 'Speaker Category',
  114. },
  115. name: 'Hi Fi Speakers',
  116. });
  117. });
  118. it('updateFacetValues', async () => {
  119. const result = await adminClient.query<UpdateFacetValues.Mutation, UpdateFacetValues.Variables>(
  120. UPDATE_FACET_VALUES,
  121. {
  122. input: [
  123. {
  124. id: speakerTypeFacet.values[0].id,
  125. code: 'compact',
  126. },
  127. ],
  128. },
  129. );
  130. expect(result.updateFacetValues[0].code).toBe('compact');
  131. });
  132. it('facets', async () => {
  133. const result = await adminClient.query<GetFacetList.Query>(GET_FACET_LIST);
  134. const { items } = result.facets;
  135. expect(items.length).toBe(2);
  136. expect(items[0].name).toBe('category');
  137. expect(items[1].name).toBe('Speaker Category');
  138. brandFacet = items[0];
  139. speakerTypeFacet = items[1];
  140. });
  141. it('facet', async () => {
  142. const result = await adminClient.query<GetFacetWithValues.Query, GetFacetWithValues.Variables>(
  143. GET_FACET_WITH_VALUES,
  144. {
  145. id: speakerTypeFacet.id,
  146. },
  147. );
  148. expect(result.facet!.name).toBe('Speaker Category');
  149. });
  150. describe('deletion', () => {
  151. let products: GetProductListWithVariants.Items[];
  152. beforeAll(async () => {
  153. // add the FacetValues to products and variants
  154. const result1 = await adminClient.query<GetProductListWithVariants.Query>(
  155. GET_PRODUCTS_LIST_WITH_VARIANTS,
  156. );
  157. products = result1.products.items;
  158. await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
  159. input: {
  160. id: products[0].id,
  161. facetValueIds: [speakerTypeFacet.values[0].id],
  162. },
  163. });
  164. await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
  165. UPDATE_PRODUCT_VARIANTS,
  166. {
  167. input: [
  168. {
  169. id: products[0].variants[0].id,
  170. facetValueIds: [speakerTypeFacet.values[0].id],
  171. },
  172. ],
  173. },
  174. );
  175. await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
  176. input: {
  177. id: products[1].id,
  178. facetValueIds: [speakerTypeFacet.values[1].id],
  179. },
  180. });
  181. });
  182. it('deleteFacetValues deletes unused facetValue', async () => {
  183. const facetValueToDelete = speakerTypeFacet.values[2];
  184. const result1 = await adminClient.query<DeleteFacetValues.Mutation, DeleteFacetValues.Variables>(
  185. DELETE_FACET_VALUES,
  186. {
  187. ids: [facetValueToDelete.id],
  188. force: false,
  189. },
  190. );
  191. const result2 = await adminClient.query<GetFacetWithValues.Query, GetFacetWithValues.Variables>(
  192. GET_FACET_WITH_VALUES,
  193. {
  194. id: speakerTypeFacet.id,
  195. },
  196. );
  197. expect(result1.deleteFacetValues).toEqual([
  198. {
  199. result: DeletionResult.DELETED,
  200. message: ``,
  201. },
  202. ]);
  203. expect(result2.facet!.values[0]).not.toEqual(facetValueToDelete);
  204. });
  205. it('deleteFacetValues for FacetValue in use returns NOT_DELETED', async () => {
  206. const facetValueToDelete = speakerTypeFacet.values[0];
  207. const result1 = await adminClient.query<DeleteFacetValues.Mutation, DeleteFacetValues.Variables>(
  208. DELETE_FACET_VALUES,
  209. {
  210. ids: [facetValueToDelete.id],
  211. force: false,
  212. },
  213. );
  214. const result2 = await adminClient.query<GetFacetWithValues.Query, GetFacetWithValues.Variables>(
  215. GET_FACET_WITH_VALUES,
  216. {
  217. id: speakerTypeFacet.id,
  218. },
  219. );
  220. expect(result1.deleteFacetValues).toEqual([
  221. {
  222. result: DeletionResult.NOT_DELETED,
  223. message: `The selected FacetValue is assigned to 1 Product, 1 ProductVariant`,
  224. },
  225. ]);
  226. expect(result2.facet!.values.find(v => v.id === facetValueToDelete.id)).toBeDefined();
  227. });
  228. it('deleteFacetValues for FacetValue in use can be force deleted', async () => {
  229. const facetValueToDelete = speakerTypeFacet.values[0];
  230. const result1 = await adminClient.query<DeleteFacetValues.Mutation, DeleteFacetValues.Variables>(
  231. DELETE_FACET_VALUES,
  232. {
  233. ids: [facetValueToDelete.id],
  234. force: true,
  235. },
  236. );
  237. expect(result1.deleteFacetValues).toEqual([
  238. {
  239. result: DeletionResult.DELETED,
  240. message: `The selected FacetValue was removed from 1 Product, 1 ProductVariant and deleted`,
  241. },
  242. ]);
  243. // FacetValue no longer in the Facet.values array
  244. const result2 = await adminClient.query<GetFacetWithValues.Query, GetFacetWithValues.Variables>(
  245. GET_FACET_WITH_VALUES,
  246. {
  247. id: speakerTypeFacet.id,
  248. },
  249. );
  250. expect(result2.facet!.values[0]).not.toEqual(facetValueToDelete);
  251. // FacetValue no longer in the Product.facetValues array
  252. const result3 = await adminClient.query<
  253. GetProductWithVariants.Query,
  254. GetProductWithVariants.Variables
  255. >(GET_PRODUCT_WITH_VARIANTS, {
  256. id: products[0].id,
  257. });
  258. expect(result3.product!.facetValues).toEqual([]);
  259. });
  260. it('deleteFacet that is in use returns NOT_DELETED', async () => {
  261. const result1 = await adminClient.query<DeleteFacet.Mutation, DeleteFacet.Variables>(
  262. DELETE_FACET,
  263. {
  264. id: speakerTypeFacet.id,
  265. force: false,
  266. },
  267. );
  268. const result2 = await adminClient.query<GetFacetWithValues.Query, GetFacetWithValues.Variables>(
  269. GET_FACET_WITH_VALUES,
  270. {
  271. id: speakerTypeFacet.id,
  272. },
  273. );
  274. expect(result1.deleteFacet).toEqual({
  275. result: DeletionResult.NOT_DELETED,
  276. message: `The selected Facet includes FacetValues which are assigned to 1 Product`,
  277. });
  278. expect(result2.facet).not.toBe(null);
  279. });
  280. it('deleteFacet that is in use can be force deleted', async () => {
  281. const result1 = await adminClient.query<DeleteFacet.Mutation, DeleteFacet.Variables>(
  282. DELETE_FACET,
  283. {
  284. id: speakerTypeFacet.id,
  285. force: true,
  286. },
  287. );
  288. expect(result1.deleteFacet).toEqual({
  289. result: DeletionResult.DELETED,
  290. message: `The Facet was deleted and its FacetValues were removed from 1 Product`,
  291. });
  292. // FacetValue no longer in the Facet.values array
  293. const result2 = await adminClient.query<GetFacetWithValues.Query, GetFacetWithValues.Variables>(
  294. GET_FACET_WITH_VALUES,
  295. {
  296. id: speakerTypeFacet.id,
  297. },
  298. );
  299. expect(result2.facet).toBe(null);
  300. // FacetValue no longer in the Product.facetValues array
  301. const result3 = await adminClient.query<
  302. GetProductWithVariants.Query,
  303. GetProductWithVariants.Variables
  304. >(GET_PRODUCT_WITH_VARIANTS, {
  305. id: products[1].id,
  306. });
  307. expect(result3.product!.facetValues).toEqual([]);
  308. });
  309. it('deleteFacet with no FacetValues works', async () => {
  310. const { createFacet } = await adminClient.query<CreateFacet.Mutation, CreateFacet.Variables>(
  311. CREATE_FACET,
  312. {
  313. input: {
  314. code: 'test',
  315. isPrivate: false,
  316. translations: [{ languageCode: LanguageCode.en, name: 'Test' }],
  317. },
  318. },
  319. );
  320. const result = await adminClient.query<DeleteFacet.Mutation, DeleteFacet.Variables>(
  321. DELETE_FACET,
  322. {
  323. id: createFacet.id,
  324. force: false,
  325. },
  326. );
  327. expect(result.deleteFacet.result).toBe(DeletionResult.DELETED);
  328. });
  329. });
  330. describe('channels', () => {
  331. const SECOND_CHANNEL_TOKEN = 'second_channel_token';
  332. let createdFacet: CreateFacet.CreateFacet;
  333. beforeAll(async () => {
  334. const { createChannel } = await adminClient.query<
  335. CreateChannel.Mutation,
  336. CreateChannel.Variables
  337. >(CREATE_CHANNEL, {
  338. input: {
  339. code: 'second-channel',
  340. token: SECOND_CHANNEL_TOKEN,
  341. defaultLanguageCode: LanguageCode.en,
  342. currencyCode: CurrencyCode.USD,
  343. pricesIncludeTax: true,
  344. defaultShippingZoneId: 'T_1',
  345. defaultTaxZoneId: 'T_1',
  346. },
  347. });
  348. const { assignProductsToChannel } = await adminClient.query<
  349. AssignProductsToChannel.Mutation,
  350. AssignProductsToChannel.Variables
  351. >(ASSIGN_PRODUCT_TO_CHANNEL, {
  352. input: {
  353. channelId: (createChannel as ChannelFragment).id,
  354. productIds: ['T_1'],
  355. priceFactor: 0.5,
  356. },
  357. });
  358. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  359. });
  360. it('create Facet in channel', async () => {
  361. const { createFacet } = await adminClient.query<CreateFacet.Mutation, CreateFacet.Variables>(
  362. CREATE_FACET,
  363. {
  364. input: {
  365. isPrivate: false,
  366. code: 'channel-facet',
  367. translations: [{ languageCode: LanguageCode.en, name: 'Channel Facet' }],
  368. values: [
  369. {
  370. code: 'channel-value-1',
  371. translations: [{ languageCode: LanguageCode.en, name: 'Channel Value 1' }],
  372. },
  373. {
  374. code: 'channel-value-2',
  375. translations: [{ languageCode: LanguageCode.en, name: 'Channel Value 2' }],
  376. },
  377. ],
  378. },
  379. },
  380. );
  381. expect(createFacet.code).toBe('channel-facet');
  382. createdFacet = createFacet;
  383. });
  384. it('facets list in channel', async () => {
  385. const result = await adminClient.query<GetFacetList.Query>(GET_FACET_LIST);
  386. const { items } = result.facets;
  387. expect(items.length).toBe(1);
  388. expect(items.map(i => i.code)).toEqual(['channel-facet']);
  389. });
  390. it('Product.facetValues in channel', async () => {
  391. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  392. await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
  393. input: {
  394. id: 'T_1',
  395. facetValueIds: [brandFacet.values[0].id, ...createdFacet.values.map(v => v.id)],
  396. },
  397. });
  398. await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
  399. UPDATE_PRODUCT_VARIANTS,
  400. {
  401. input: [
  402. {
  403. id: 'T_1',
  404. facetValueIds: [brandFacet.values[0].id, ...createdFacet.values.map(v => v.id)],
  405. },
  406. ],
  407. },
  408. );
  409. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  410. const { product } = await adminClient.query<
  411. GetProductWithVariants.Query,
  412. GetProductWithVariants.Variables
  413. >(GET_PRODUCT_WITH_VARIANTS, {
  414. id: 'T_1',
  415. });
  416. expect(product?.facetValues.map(fv => fv.code).sort()).toEqual([
  417. 'channel-value-1',
  418. 'channel-value-2',
  419. ]);
  420. });
  421. it('ProductVariant.facetValues in channel', async () => {
  422. const { product } = await adminClient.query<
  423. GetProductWithVariants.Query,
  424. GetProductWithVariants.Variables
  425. >(GET_PRODUCT_WITH_VARIANTS, {
  426. id: 'T_1',
  427. });
  428. expect(product?.variants[0].facetValues.map(fv => fv.code).sort()).toEqual([
  429. 'channel-value-1',
  430. 'channel-value-2',
  431. ]);
  432. });
  433. it('updating Product facetValuesIds in channel only affects that channel', async () => {
  434. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  435. await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
  436. input: {
  437. id: 'T_1',
  438. facetValueIds: [createdFacet.values[0].id],
  439. },
  440. });
  441. const { product: productC2 } = await adminClient.query<
  442. GetProductWithVariants.Query,
  443. GetProductWithVariants.Variables
  444. >(GET_PRODUCT_WITH_VARIANTS, {
  445. id: 'T_1',
  446. });
  447. expect(productC2?.facetValues.map(fv => fv.code)).toEqual([createdFacet.values[0].code]);
  448. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  449. const { product: productCD } = await adminClient.query<
  450. GetProductWithVariants.Query,
  451. GetProductWithVariants.Variables
  452. >(GET_PRODUCT_WITH_VARIANTS, {
  453. id: 'T_1',
  454. });
  455. expect(productCD?.facetValues.map(fv => fv.code)).toEqual([
  456. brandFacet.values[0].code,
  457. createdFacet.values[0].code,
  458. ]);
  459. });
  460. it('updating ProductVariant facetValuesIds in channel only affects that channel', async () => {
  461. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  462. await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
  463. UPDATE_PRODUCT_VARIANTS,
  464. {
  465. input: [
  466. {
  467. id: 'T_1',
  468. facetValueIds: [createdFacet.values[0].id],
  469. },
  470. ],
  471. },
  472. );
  473. const { product: productC2 } = await adminClient.query<
  474. GetProductWithVariants.Query,
  475. GetProductWithVariants.Variables
  476. >(GET_PRODUCT_WITH_VARIANTS, {
  477. id: 'T_1',
  478. });
  479. expect(productC2?.variants.find(v => v.id === 'T_1')?.facetValues.map(fv => fv.code)).toEqual([
  480. createdFacet.values[0].code,
  481. ]);
  482. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  483. const { product: productCD } = await adminClient.query<
  484. GetProductWithVariants.Query,
  485. GetProductWithVariants.Variables
  486. >(GET_PRODUCT_WITH_VARIANTS, {
  487. id: 'T_1',
  488. });
  489. expect(productCD?.variants.find(v => v.id === 'T_1')?.facetValues.map(fv => fv.code)).toEqual([
  490. brandFacet.values[0].code,
  491. createdFacet.values[0].code,
  492. ]);
  493. });
  494. it(
  495. 'attempting to create FacetValue in Facet from another Channel throws',
  496. assertThrowsWithMessage(async () => {
  497. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  498. await adminClient.query<CreateFacetValues.Mutation, CreateFacetValues.Variables>(
  499. CREATE_FACET_VALUES,
  500. {
  501. input: [
  502. {
  503. facetId: brandFacet.id,
  504. code: 'channel-brand',
  505. translations: [{ languageCode: LanguageCode.en, name: 'Channel Brand' }],
  506. },
  507. ],
  508. },
  509. );
  510. }, `No Facet with the id '1' could be found`),
  511. );
  512. });
  513. });
  514. export const GET_FACET_WITH_VALUES = gql`
  515. query GetFacetWithValues($id: ID!) {
  516. facet(id: $id) {
  517. ...FacetWithValues
  518. }
  519. }
  520. ${FACET_WITH_VALUES_FRAGMENT}
  521. `;
  522. const DELETE_FACET_VALUES = gql`
  523. mutation DeleteFacetValues($ids: [ID!]!, $force: Boolean) {
  524. deleteFacetValues(ids: $ids, force: $force) {
  525. result
  526. message
  527. }
  528. }
  529. `;
  530. const DELETE_FACET = gql`
  531. mutation DeleteFacet($id: ID!, $force: Boolean) {
  532. deleteFacet(id: $id, force: $force) {
  533. result
  534. message
  535. }
  536. }
  537. `;
  538. const GET_PRODUCTS_LIST_WITH_VARIANTS = gql`
  539. query GetProductListWithVariants {
  540. products {
  541. items {
  542. id
  543. name
  544. variants {
  545. id
  546. name
  547. }
  548. }
  549. totalItems
  550. }
  551. }
  552. `;
  553. export const CREATE_FACET_VALUES = gql`
  554. mutation CreateFacetValues($input: [CreateFacetValueInput!]!) {
  555. createFacetValues(input: $input) {
  556. ...FacetValue
  557. }
  558. }
  559. ${FACET_VALUE_FRAGMENT}
  560. `;
  561. export const UPDATE_FACET_VALUES = gql`
  562. mutation UpdateFacetValues($input: [UpdateFacetValueInput!]!) {
  563. updateFacetValues(input: $input) {
  564. ...FacetValue
  565. }
  566. }
  567. ${FACET_VALUE_FRAGMENT}
  568. `;