diff --git a/.changeset/silly-hotels-confess.md b/.changeset/silly-hotels-confess.md new file mode 100644 index 000000000..dd4cabe97 --- /dev/null +++ b/.changeset/silly-hotels-confess.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": patch +--- + +Experimental filters: add unit tests and bump UI library diff --git a/package-lock.json b/package-lock.json index ce50135e3..7cd441350 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "@material-ui/lab": "^4.0.0-alpha.61", "@material-ui/styles": "^4.11.4", "@reach/auto-id": "^0.16.0", - "@saleor/macaw-ui": "0.8.0-pre.112", + "@saleor/macaw-ui": "0.8.0-pre.113", "@saleor/sdk": "0.6.0", "@sentry/react": "^6.0.0", "@types/faker": "^5.1.6", @@ -8495,9 +8495,9 @@ } }, "node_modules/@saleor/macaw-ui": { - "version": "0.8.0-pre.112", - "resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.112.tgz", - "integrity": "sha512-USVepQkr3t4/6WWTyHOJ+vj6bxoPHUI0m7e13ZF012YJT1c+ORRtJtYnnKdxazCDl7GjEH1shDJmpexQmtqPJQ==", + "version": "0.8.0-pre.113", + "resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.113.tgz", + "integrity": "sha512-DJOOUIT9oaH4F/i+f37kdzoVcFggqx2JaXmUWYtx8CYMJyJpJPQuLPe734FDA9On3HnRlNs54NHDW3m7VLJM/A==", "dependencies": { "@dessert-box/react": "^0.4.0", "@floating-ui/react-dom-interactions": "^0.5.0", @@ -40717,9 +40717,9 @@ } }, "@saleor/macaw-ui": { - "version": "0.8.0-pre.112", - "resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.112.tgz", - "integrity": "sha512-USVepQkr3t4/6WWTyHOJ+vj6bxoPHUI0m7e13ZF012YJT1c+ORRtJtYnnKdxazCDl7GjEH1shDJmpexQmtqPJQ==", + "version": "0.8.0-pre.113", + "resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.113.tgz", + "integrity": "sha512-DJOOUIT9oaH4F/i+f37kdzoVcFggqx2JaXmUWYtx8CYMJyJpJPQuLPe734FDA9On3HnRlNs54NHDW3m7VLJM/A==", "requires": { "@dessert-box/react": "^0.4.0", "@floating-ui/react-dom-interactions": "^0.5.0", diff --git a/package.json b/package.json index 018f0de8b..e7c157054 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@material-ui/lab": "^4.0.0-alpha.61", "@material-ui/styles": "^4.11.4", "@reach/auto-id": "^0.16.0", - "@saleor/macaw-ui": "0.8.0-pre.112", + "@saleor/macaw-ui": "0.8.0-pre.113", "@saleor/sdk": "0.6.0", "@sentry/react": "^6.0.0", "@types/faker": "^5.1.6", diff --git a/src/components/ConditionalFilter/API/InitalStateResponse.test.ts b/src/components/ConditionalFilter/API/InitalStateResponse.test.ts index 7de3f603f..b6b05d622 100644 --- a/src/components/ConditionalFilter/API/InitalStateResponse.test.ts +++ b/src/components/ConditionalFilter/API/InitalStateResponse.test.ts @@ -3,6 +3,7 @@ import { InitialStateResponse } from "./InitialStateResponse"; describe("ConditionalFilter / API / InitialStateResponse", () => { it("should filter by dynamic attribute token", () => { + // Arrange const initialState = InitialStateResponse.empty(); initialState.attribute = { "attribute-1": { @@ -22,11 +23,14 @@ describe("ConditionalFilter / API / InitialStateResponse", () => { const expectedOutput = [ { label: "Choice 1", slug: "choice-1", value: "value-1" }, ]; + // Act const result = initialState.filterByUrlToken(token); + // Assert expect(result).toEqual(expectedOutput); }); it("should filter by static token type", () => { + // Arrange const initialState = InitialStateResponse.empty(); initialState.category = [ { label: "Category 1", value: "1", slug: "category-1" }, @@ -35,11 +39,14 @@ describe("ConditionalFilter / API / InitialStateResponse", () => { new UrlEntry("s0.category-1", "category-1"), ); const expectedOutput = ["category-1"]; + // Act const result = initialState.filterByUrlToken(token); + // Assert expect(result).toEqual(expectedOutput); }); it("should filter by boolean attribute token", () => { + // Arrange const initialState = InitialStateResponse.empty(); initialState.attribute = { "attribute-2": { @@ -60,11 +67,14 @@ describe("ConditionalFilter / API / InitialStateResponse", () => { type: "BOOLEAN", value: "true", }; + // Act const result = initialState.filterByUrlToken(token); + // Assert expect(result).toEqual(expectedOutput); }); it("should filter by static attribute token", () => { + // Arrange const initialState = InitialStateResponse.empty(); initialState.attribute = { size: { @@ -75,9 +85,10 @@ describe("ConditionalFilter / API / InitialStateResponse", () => { choices: [], }, }; - const token = UrlToken.fromUrlEntry(new UrlEntry("n0.size", "123")); + // Act const result = initialState.filterByUrlToken(token); + // Assert expect(result).toEqual("123"); }); }); diff --git a/src/components/ConditionalFilter/API/initialState/helpers.test.ts b/src/components/ConditionalFilter/API/initialState/helpers.test.ts index e6a11f7e0..a03e89da9 100644 --- a/src/components/ConditionalFilter/API/initialState/helpers.test.ts +++ b/src/components/ConditionalFilter/API/initialState/helpers.test.ts @@ -10,92 +10,96 @@ import { import { createInitialStateFromData } from "./helpers"; describe("ConditionalFilter / API / createInitialStateFromData", () => { - const channelQuery = { - data: { - channels: [ - { id: "1", name: "Channel 1", slug: "channel-1" }, - { id: "2", name: "Channel 2", slug: "channel-2" }, - ], - }, - } as ApolloQueryResult<_GetChannelOperandsQuery>; - - const collectionQuery = { - data: { - collections: { - edges: [ - { node: { id: "1", name: "Collection 1", slug: "collection-1" } }, - { node: { id: "2", name: "Collection 2", slug: "collection-2" } }, + it("should create initial state from queries", () => { + // Arrange + const channelQuery = { + data: { + channels: [ + { id: "1", name: "Channel 1", slug: "channel-1" }, + { id: "2", name: "Channel 2", slug: "channel-2" }, ], }, - }, - } as ApolloQueryResult<_SearchCollectionsOperandsQuery>; + } as ApolloQueryResult<_GetChannelOperandsQuery>; - const categoryQuery = { - data: { - categories: { - edges: [ - { node: { id: "1", name: "Category 1", slug: "category-1" } }, - { node: { id: "2", name: "Category 2", slug: "category-2" } }, - ], + const collectionQuery = { + data: { + collections: { + edges: [ + { node: { id: "1", name: "Collection 1", slug: "collection-1" } }, + { node: { id: "2", name: "Collection 2", slug: "collection-2" } }, + ], + }, }, - }, - } as ApolloQueryResult<_SearchCategoriesOperandsQuery>; + } as ApolloQueryResult<_SearchCollectionsOperandsQuery>; - const productTypeQuery = { - data: { - productTypes: { - edges: [ - { node: { id: "1", name: "Product Type 1", slug: "product-type-1" } }, - { node: { id: "2", name: "Product Type 2", slug: "product-type-2" } }, - ], + const categoryQuery = { + data: { + categories: { + edges: [ + { node: { id: "1", name: "Category 1", slug: "category-1" } }, + { node: { id: "2", name: "Category 2", slug: "category-2" } }, + ], + }, }, - }, - } as ApolloQueryResult<_SearchProductTypesOperandsQuery>; + } as ApolloQueryResult<_SearchCategoriesOperandsQuery>; - const attributeQuery = { - data: { - attributes: { - edges: [ - { - node: { - id: "1", - name: "Attribute 1", - slug: "attribute-1", - inputType: "MULTISELECT", - choices: { - edges: [ - { - node: { - id: "1", - name: "Choice 1", - slug: "choice-1", + const productTypeQuery = { + data: { + productTypes: { + edges: [ + { + node: { id: "1", name: "Product Type 1", slug: "product-type-1" }, + }, + { + node: { id: "2", name: "Product Type 2", slug: "product-type-2" }, + }, + ], + }, + }, + } as ApolloQueryResult<_SearchProductTypesOperandsQuery>; + + const attributeQuery = { + data: { + attributes: { + edges: [ + { + node: { + id: "1", + name: "Attribute 1", + slug: "attribute-1", + inputType: "MULTISELECT", + choices: { + edges: [ + { + node: { + id: "1", + name: "Choice 1", + slug: "choice-1", + }, }, - }, - { - node: { - id: "2", - name: "Choice 2", - slug: "choice-2", + { + node: { + id: "2", + name: "Choice 2", + slug: "choice-2", + }, }, - }, - ], + ], + }, }, }, - }, - { - node: { - id: "2", - name: "Attribute 2", - slug: "attribute-2", - inputType: "BOOLEAN", + { + node: { + id: "2", + name: "Attribute 2", + slug: "attribute-2", + inputType: "BOOLEAN", + }, }, - }, - ], + ], + }, }, - }, - } as ApolloQueryResult<_SearchAttributeOperandsQuery>; - - it("should create initial state from queries", () => { + } as ApolloQueryResult<_SearchAttributeOperandsQuery>; const data = [ channelQuery, collectionQuery, @@ -104,8 +108,9 @@ describe("ConditionalFilter / API / createInitialStateFromData", () => { attributeQuery, ]; const channel = ["channel-1"]; - + // Act const result = createInitialStateFromData(data, channel); + // Assert expect(result).toMatchSnapshot(); }); }); diff --git a/src/components/ConditionalFilter/FilterElement/Condition.ts b/src/components/ConditionalFilter/FilterElement/Condition.ts index 0938db6c3..bed54c22f 100644 --- a/src/components/ConditionalFilter/FilterElement/Condition.ts +++ b/src/components/ConditionalFilter/FilterElement/Condition.ts @@ -6,7 +6,7 @@ import { ConditionSelected } from "./ConditionSelected"; import { ItemOption } from "./ConditionValue"; export class Condition { - private constructor( + public constructor( public options: ConditionOptions, public selected: ConditionSelected, public loading: boolean, @@ -79,7 +79,7 @@ export class Condition { if (token.isAttribute()) { const attribute = response.attributeByName(token.name); - const options = ConditionOptions.fromAtributeType(attribute.inputType); + const options = ConditionOptions.fromAttributeType(attribute.inputType); const option = options.find(item => item.label === token.conditionKind)!; const value = response.filterByUrlToken(token); diff --git a/src/components/ConditionalFilter/FilterElement/ConditionOptions.ts b/src/components/ConditionalFilter/FilterElement/ConditionOptions.ts index 3d9a8a8df..6cf74c0b1 100644 --- a/src/components/ConditionalFilter/FilterElement/ConditionOptions.ts +++ b/src/components/ConditionalFilter/FilterElement/ConditionOptions.ts @@ -32,7 +32,7 @@ export class ConditionOptions extends Array { return name in ATTRIBUTE_INPUT_TYPE_CONDITIONS; } - public static fromAtributeType(inputType: AttributeInputType) { + public static fromAttributeType(inputType: AttributeInputType) { const options = ATTRIBUTE_INPUT_TYPE_CONDITIONS[inputType]; if (!options) { @@ -75,7 +75,7 @@ export class ConditionOptions extends Array { } public isEmpty() { - return this.length === 0 + return this.length === 0; } public findByLabel(label: string) { diff --git a/src/components/ConditionalFilter/FilterElement/ConditionSelected.ts b/src/components/ConditionalFilter/FilterElement/ConditionSelected.ts index bbdccb296..56e8681e5 100644 --- a/src/components/ConditionalFilter/FilterElement/ConditionSelected.ts +++ b/src/components/ConditionalFilter/FilterElement/ConditionSelected.ts @@ -3,7 +3,7 @@ import { ConditionItem } from "./ConditionOptions"; import { ConditionValue, isItemOptionArray, isTuple } from "./ConditionValue"; export class ConditionSelected { - private constructor( + public constructor( public value: ConditionValue, public conditionValue: ConditionItem | null, public options: ConditionValue[], diff --git a/src/components/ConditionalFilter/FilterElement/FilterElement.ts b/src/components/ConditionalFilter/FilterElement/FilterElement.ts index c50c3ea77..b1e7d1960 100644 --- a/src/components/ConditionalFilter/FilterElement/FilterElement.ts +++ b/src/components/ConditionalFilter/FilterElement/FilterElement.ts @@ -12,7 +12,7 @@ import { ConditionSelected } from "./ConditionSelected"; import { ConditionValue, ItemOption } from "./ConditionValue"; import { Constraint } from "./Constraint"; -class ExpressionValue { +export class ExpressionValue { constructor( public value: string, public label: string, @@ -68,7 +68,7 @@ class ExpressionValue { } export class FilterElement { - private constructor( + public constructor( public value: ExpressionValue, public condition: Condition, public loading: boolean, @@ -165,7 +165,7 @@ export class FilterElement { } public equals(element: FilterElement) { - return this.value.value === element.value.value + return this.value.value === element.value.value; } public static isCompatible(element: unknown): element is FilterElement { diff --git a/src/components/ConditionalFilter/ValueProvider/TokenArray/TokenArray.test.ts b/src/components/ConditionalFilter/ValueProvider/TokenArray/TokenArray.test.ts new file mode 100644 index 000000000..8ecaed833 --- /dev/null +++ b/src/components/ConditionalFilter/ValueProvider/TokenArray/TokenArray.test.ts @@ -0,0 +1,49 @@ +import { TokenArray } from "."; + +describe("ConditionalFilter / ValueProvider / TokenArray", () => { + it("should parse empty params", () => { + // Arrange + const url = new TokenArray(""); + // Act + const fetchingParams = url.getFetchingParams(); + // Assert + expect(fetchingParams).toEqual({ + category: [], + collection: [], + channel: [], + productType: [], + attribute: {}, + }); + }); + + it("should parse params with values", () => { + // Arrange + const params = new URLSearchParams({ + "0[s0.price]": "123", + "1": "AND", + "2[s0.channel]": "channel-pln", + "3": "OR", + "4[s2.collection][0]": "featured-products", + "5": "AND", + "6[s0.productType]": "beer", + "7": "AND", + "8[s2.category][0]": "accessories", + "9[s2.category][1]": "groceries", + "10": "AND", + "11[o2.bottle-size][0]": "attribute-id", + }); + // Act + const url = new TokenArray(params.toString()); + const fetchingParams = url.getFetchingParams(); + // Assert + expect(fetchingParams).toEqual({ + attribute: { + "bottle-size": ["attribute-id"], + }, + category: ["accessories", "groceries"], + channel: ["channel-pln"], + collection: ["featured-products"], + productType: ["beer"], + }); + }); +}); diff --git a/src/components/ConditionalFilter/queryVariables.test.ts b/src/components/ConditionalFilter/queryVariables.test.ts new file mode 100644 index 000000000..4acc9029e --- /dev/null +++ b/src/components/ConditionalFilter/queryVariables.test.ts @@ -0,0 +1,103 @@ +import { Condition, FilterContainer, FilterElement } from "./FilterElement"; +import { ConditionOptions } from "./FilterElement/ConditionOptions"; +import { ConditionSelected } from "./FilterElement/ConditionSelected"; +import { ExpressionValue } from "./FilterElement/FilterElement"; +import { createProductQueryVariables } from "./queryVariables"; + +const createConditionValue = ( + label: string, + slug: string, + value: string, + originalSlug?: string, +) => ({ + label, + slug, + value, + originalSlug, +}); +const createConditionItem = (type: string, value: string, label: string) => ({ + type, + value, + label, +}); + +const createConditionOptions = ( + label: string, + slug: string, + value: string, + originalSlug: string, +) => [ + { + label, + slug, + value, + originalSlug, + }, +]; + +describe("ConditionalFilter / queryVariables / createProductQueryVariables", () => { + it("should return empty variables for empty filters", () => { + // Arrange + const filters: FilterContainer = []; + const expectedOutput = { + attributes: [], + }; + // Act + const result = createProductQueryVariables(filters); + // Assert + expect(result).toEqual(expectedOutput); + }); + + it("should create variables with selected filters", () => { + // Arrange + const filters: FilterContainer = [ + new FilterElement( + new ExpressionValue("price", "Price", "price"), + new Condition( + ConditionOptions.fromStaticElementName("price"), + new ConditionSelected( + createConditionValue("price", "price", "123"), + createConditionItem("price", "123", "Price"), + [], + false, + ), + false, + ), + false, + ), + "AND", + new FilterElement( + new ExpressionValue("bottle-size", "Bottle size", "DROPDOWN"), + new Condition( + ConditionOptions.fromAttributeType("DROPDOWN"), + new ConditionSelected( + createConditionValue( + "bottle-size", + "bottle-id", + "bottle-id", + "0-5l", + ), + createConditionItem("DROPDOWN", "bottle-id", "Bottle size"), + createConditionOptions( + "bottle-size", + "bottle-id", + "bottle-id", + "0-5l", + ), + false, + ), + false, + ), + false, + ), + ]; + const expectedOutput = { + attributes: [{ slug: "bottle-size", values: ["0-5l"] }], + price: { eq: "123" }, + }; + // Act + const result = createProductQueryVariables(filters); + // Assert + expect(result).toEqual(expectedOutput); + }); +});