Experimental filters: add unit tests and bump UI library (#4045)

This commit is contained in:
Krzysztof Żuraw 2023-08-02 10:21:18 +02:00 committed by GitHub
parent a8794d41a8
commit ae813a406e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 264 additions and 91 deletions

View file

@ -0,0 +1,5 @@
---
"saleor-dashboard": patch
---
Experimental filters: add unit tests and bump UI library

14
package-lock.json generated
View file

@ -27,7 +27,7 @@
"@material-ui/lab": "^4.0.0-alpha.61", "@material-ui/lab": "^4.0.0-alpha.61",
"@material-ui/styles": "^4.11.4", "@material-ui/styles": "^4.11.4",
"@reach/auto-id": "^0.16.0", "@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", "@saleor/sdk": "0.6.0",
"@sentry/react": "^6.0.0", "@sentry/react": "^6.0.0",
"@types/faker": "^5.1.6", "@types/faker": "^5.1.6",
@ -8495,9 +8495,9 @@
} }
}, },
"node_modules/@saleor/macaw-ui": { "node_modules/@saleor/macaw-ui": {
"version": "0.8.0-pre.112", "version": "0.8.0-pre.113",
"resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.112.tgz", "resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.113.tgz",
"integrity": "sha512-USVepQkr3t4/6WWTyHOJ+vj6bxoPHUI0m7e13ZF012YJT1c+ORRtJtYnnKdxazCDl7GjEH1shDJmpexQmtqPJQ==", "integrity": "sha512-DJOOUIT9oaH4F/i+f37kdzoVcFggqx2JaXmUWYtx8CYMJyJpJPQuLPe734FDA9On3HnRlNs54NHDW3m7VLJM/A==",
"dependencies": { "dependencies": {
"@dessert-box/react": "^0.4.0", "@dessert-box/react": "^0.4.0",
"@floating-ui/react-dom-interactions": "^0.5.0", "@floating-ui/react-dom-interactions": "^0.5.0",
@ -40717,9 +40717,9 @@
} }
}, },
"@saleor/macaw-ui": { "@saleor/macaw-ui": {
"version": "0.8.0-pre.112", "version": "0.8.0-pre.113",
"resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.112.tgz", "resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.113.tgz",
"integrity": "sha512-USVepQkr3t4/6WWTyHOJ+vj6bxoPHUI0m7e13ZF012YJT1c+ORRtJtYnnKdxazCDl7GjEH1shDJmpexQmtqPJQ==", "integrity": "sha512-DJOOUIT9oaH4F/i+f37kdzoVcFggqx2JaXmUWYtx8CYMJyJpJPQuLPe734FDA9On3HnRlNs54NHDW3m7VLJM/A==",
"requires": { "requires": {
"@dessert-box/react": "^0.4.0", "@dessert-box/react": "^0.4.0",
"@floating-ui/react-dom-interactions": "^0.5.0", "@floating-ui/react-dom-interactions": "^0.5.0",

View file

@ -34,7 +34,7 @@
"@material-ui/lab": "^4.0.0-alpha.61", "@material-ui/lab": "^4.0.0-alpha.61",
"@material-ui/styles": "^4.11.4", "@material-ui/styles": "^4.11.4",
"@reach/auto-id": "^0.16.0", "@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", "@saleor/sdk": "0.6.0",
"@sentry/react": "^6.0.0", "@sentry/react": "^6.0.0",
"@types/faker": "^5.1.6", "@types/faker": "^5.1.6",

View file

@ -3,6 +3,7 @@ import { InitialStateResponse } from "./InitialStateResponse";
describe("ConditionalFilter / API / InitialStateResponse", () => { describe("ConditionalFilter / API / InitialStateResponse", () => {
it("should filter by dynamic attribute token", () => { it("should filter by dynamic attribute token", () => {
// Arrange
const initialState = InitialStateResponse.empty(); const initialState = InitialStateResponse.empty();
initialState.attribute = { initialState.attribute = {
"attribute-1": { "attribute-1": {
@ -22,11 +23,14 @@ describe("ConditionalFilter / API / InitialStateResponse", () => {
const expectedOutput = [ const expectedOutput = [
{ label: "Choice 1", slug: "choice-1", value: "value-1" }, { label: "Choice 1", slug: "choice-1", value: "value-1" },
]; ];
// Act
const result = initialState.filterByUrlToken(token); const result = initialState.filterByUrlToken(token);
// Assert
expect(result).toEqual(expectedOutput); expect(result).toEqual(expectedOutput);
}); });
it("should filter by static token type", () => { it("should filter by static token type", () => {
// Arrange
const initialState = InitialStateResponse.empty(); const initialState = InitialStateResponse.empty();
initialState.category = [ initialState.category = [
{ label: "Category 1", value: "1", slug: "category-1" }, { label: "Category 1", value: "1", slug: "category-1" },
@ -35,11 +39,14 @@ describe("ConditionalFilter / API / InitialStateResponse", () => {
new UrlEntry("s0.category-1", "category-1"), new UrlEntry("s0.category-1", "category-1"),
); );
const expectedOutput = ["category-1"]; const expectedOutput = ["category-1"];
// Act
const result = initialState.filterByUrlToken(token); const result = initialState.filterByUrlToken(token);
// Assert
expect(result).toEqual(expectedOutput); expect(result).toEqual(expectedOutput);
}); });
it("should filter by boolean attribute token", () => { it("should filter by boolean attribute token", () => {
// Arrange
const initialState = InitialStateResponse.empty(); const initialState = InitialStateResponse.empty();
initialState.attribute = { initialState.attribute = {
"attribute-2": { "attribute-2": {
@ -60,11 +67,14 @@ describe("ConditionalFilter / API / InitialStateResponse", () => {
type: "BOOLEAN", type: "BOOLEAN",
value: "true", value: "true",
}; };
// Act
const result = initialState.filterByUrlToken(token); const result = initialState.filterByUrlToken(token);
// Assert
expect(result).toEqual(expectedOutput); expect(result).toEqual(expectedOutput);
}); });
it("should filter by static attribute token", () => { it("should filter by static attribute token", () => {
// Arrange
const initialState = InitialStateResponse.empty(); const initialState = InitialStateResponse.empty();
initialState.attribute = { initialState.attribute = {
size: { size: {
@ -75,9 +85,10 @@ describe("ConditionalFilter / API / InitialStateResponse", () => {
choices: [], choices: [],
}, },
}; };
const token = UrlToken.fromUrlEntry(new UrlEntry("n0.size", "123")); const token = UrlToken.fromUrlEntry(new UrlEntry("n0.size", "123"));
// Act
const result = initialState.filterByUrlToken(token); const result = initialState.filterByUrlToken(token);
// Assert
expect(result).toEqual("123"); expect(result).toEqual("123");
}); });
}); });

View file

@ -10,92 +10,96 @@ import {
import { createInitialStateFromData } from "./helpers"; import { createInitialStateFromData } from "./helpers";
describe("ConditionalFilter / API / createInitialStateFromData", () => { describe("ConditionalFilter / API / createInitialStateFromData", () => {
const channelQuery = { it("should create initial state from queries", () => {
data: { // Arrange
channels: [ const channelQuery = {
{ id: "1", name: "Channel 1", slug: "channel-1" }, data: {
{ id: "2", name: "Channel 2", slug: "channel-2" }, 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" } },
], ],
}, },
}, } as ApolloQueryResult<_GetChannelOperandsQuery>;
} as ApolloQueryResult<_SearchCollectionsOperandsQuery>;
const categoryQuery = { const collectionQuery = {
data: { data: {
categories: { collections: {
edges: [ edges: [
{ node: { id: "1", name: "Category 1", slug: "category-1" } }, { node: { id: "1", name: "Collection 1", slug: "collection-1" } },
{ node: { id: "2", name: "Category 2", slug: "category-2" } }, { node: { id: "2", name: "Collection 2", slug: "collection-2" } },
], ],
},
}, },
}, } as ApolloQueryResult<_SearchCollectionsOperandsQuery>;
} as ApolloQueryResult<_SearchCategoriesOperandsQuery>;
const productTypeQuery = { const categoryQuery = {
data: { data: {
productTypes: { categories: {
edges: [ edges: [
{ node: { id: "1", name: "Product Type 1", slug: "product-type-1" } }, { node: { id: "1", name: "Category 1", slug: "category-1" } },
{ node: { id: "2", name: "Product Type 2", slug: "product-type-2" } }, { node: { id: "2", name: "Category 2", slug: "category-2" } },
], ],
},
}, },
}, } as ApolloQueryResult<_SearchCategoriesOperandsQuery>;
} as ApolloQueryResult<_SearchProductTypesOperandsQuery>;
const attributeQuery = { const productTypeQuery = {
data: { data: {
attributes: { productTypes: {
edges: [ edges: [
{ {
node: { node: { id: "1", name: "Product Type 1", slug: "product-type-1" },
id: "1", },
name: "Attribute 1", {
slug: "attribute-1", node: { id: "2", name: "Product Type 2", slug: "product-type-2" },
inputType: "MULTISELECT", },
choices: { ],
edges: [ },
{ },
node: { } as ApolloQueryResult<_SearchProductTypesOperandsQuery>;
id: "1",
name: "Choice 1", const attributeQuery = {
slug: "choice-1", 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: {
node: { id: "2",
id: "2", name: "Choice 2",
name: "Choice 2", slug: "choice-2",
slug: "choice-2", },
}, },
}, ],
], },
}, },
}, },
}, {
{ node: {
node: { id: "2",
id: "2", name: "Attribute 2",
name: "Attribute 2", slug: "attribute-2",
slug: "attribute-2", inputType: "BOOLEAN",
inputType: "BOOLEAN", },
}, },
}, ],
], },
}, },
}, } as ApolloQueryResult<_SearchAttributeOperandsQuery>;
} as ApolloQueryResult<_SearchAttributeOperandsQuery>;
it("should create initial state from queries", () => {
const data = [ const data = [
channelQuery, channelQuery,
collectionQuery, collectionQuery,
@ -104,8 +108,9 @@ describe("ConditionalFilter / API / createInitialStateFromData", () => {
attributeQuery, attributeQuery,
]; ];
const channel = ["channel-1"]; const channel = ["channel-1"];
// Act
const result = createInitialStateFromData(data, channel); const result = createInitialStateFromData(data, channel);
// Assert
expect(result).toMatchSnapshot(); expect(result).toMatchSnapshot();
}); });
}); });

View file

@ -6,7 +6,7 @@ import { ConditionSelected } from "./ConditionSelected";
import { ItemOption } from "./ConditionValue"; import { ItemOption } from "./ConditionValue";
export class Condition { export class Condition {
private constructor( public constructor(
public options: ConditionOptions, public options: ConditionOptions,
public selected: ConditionSelected, public selected: ConditionSelected,
public loading: boolean, public loading: boolean,
@ -79,7 +79,7 @@ export class Condition {
if (token.isAttribute()) { if (token.isAttribute()) {
const attribute = response.attributeByName(token.name); 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 option = options.find(item => item.label === token.conditionKind)!;
const value = response.filterByUrlToken(token); const value = response.filterByUrlToken(token);

View file

@ -32,7 +32,7 @@ export class ConditionOptions extends Array<ConditionItem> {
return name in ATTRIBUTE_INPUT_TYPE_CONDITIONS; return name in ATTRIBUTE_INPUT_TYPE_CONDITIONS;
} }
public static fromAtributeType(inputType: AttributeInputType) { public static fromAttributeType(inputType: AttributeInputType) {
const options = ATTRIBUTE_INPUT_TYPE_CONDITIONS[inputType]; const options = ATTRIBUTE_INPUT_TYPE_CONDITIONS[inputType];
if (!options) { if (!options) {
@ -75,7 +75,7 @@ export class ConditionOptions extends Array<ConditionItem> {
} }
public isEmpty() { public isEmpty() {
return this.length === 0 return this.length === 0;
} }
public findByLabel(label: string) { public findByLabel(label: string) {

View file

@ -3,7 +3,7 @@ import { ConditionItem } from "./ConditionOptions";
import { ConditionValue, isItemOptionArray, isTuple } from "./ConditionValue"; import { ConditionValue, isItemOptionArray, isTuple } from "./ConditionValue";
export class ConditionSelected { export class ConditionSelected {
private constructor( public constructor(
public value: ConditionValue, public value: ConditionValue,
public conditionValue: ConditionItem | null, public conditionValue: ConditionItem | null,
public options: ConditionValue[], public options: ConditionValue[],

View file

@ -12,7 +12,7 @@ import { ConditionSelected } from "./ConditionSelected";
import { ConditionValue, ItemOption } from "./ConditionValue"; import { ConditionValue, ItemOption } from "./ConditionValue";
import { Constraint } from "./Constraint"; import { Constraint } from "./Constraint";
class ExpressionValue { export class ExpressionValue {
constructor( constructor(
public value: string, public value: string,
public label: string, public label: string,
@ -68,7 +68,7 @@ class ExpressionValue {
} }
export class FilterElement { export class FilterElement {
private constructor( public constructor(
public value: ExpressionValue, public value: ExpressionValue,
public condition: Condition, public condition: Condition,
public loading: boolean, public loading: boolean,
@ -165,7 +165,7 @@ export class FilterElement {
} }
public equals(element: 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 { public static isCompatible(element: unknown): element is FilterElement {

View file

@ -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"],
});
});
});

View file

@ -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);
});
});