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/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",

View file

@ -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",

View file

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

View file

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

View file

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

View file

@ -32,7 +32,7 @@ export class ConditionOptions extends Array<ConditionItem> {
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<ConditionItem> {
}
public isEmpty() {
return this.length === 0
return this.length === 0;
}
public findByLabel(label: string) {

View file

@ -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[],

View file

@ -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 {

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