From c400c363066c3161b095c4284781e063acb5db61 Mon Sep 17 00:00:00 2001 From: Vijay Date: Fri, 2 May 2025 15:59:48 +0530 Subject: [PATCH] fix: between operator for attribute matching (#21031) * update json logic for raqb between operator * nit * add tests * e2e tests * after merge: updated tests due to latest changes in routingforms v3 --------- Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com> --- .../routing-forms/__tests__/config.test.ts | 36 ++++ .../config/BasicConfig.ts | 15 +- .../playwright/tests/basic.e2e.ts | 191 ++++++++++++++++++ 3 files changed, 241 insertions(+), 1 deletion(-) diff --git a/packages/app-store/routing-forms/__tests__/config.test.ts b/packages/app-store/routing-forms/__tests__/config.test.ts index 13b814e434..fbd4f45c82 100644 --- a/packages/app-store/routing-forms/__tests__/config.test.ts +++ b/packages/app-store/routing-forms/__tests__/config.test.ts @@ -119,5 +119,41 @@ describe("Query Builder Config", () => { it("should have maxNesting set to 1 in settings", () => { assertMaxNesting(AttributesBaseConfig, 1); }); + + it("should provide jsonlogic for between operator", () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const jsonLogic = AttributesBaseConfig.operators.between.jsonLogic( + { var: "89ee81ae-953c-409b-aacc-700e1ce5ae20" }, + "between", + ["1", "5"] + ); + + // Verifies that it is using >= and <= in implementation and integer parsed + expect(jsonLogic).toEqual({ + and: [ + { ">=": [{ var: "89ee81ae-953c-409b-aacc-700e1ce5ae20" }, 1] }, + { "<=": [{ var: "89ee81ae-953c-409b-aacc-700e1ce5ae20" }, 5] }, + ], + }); + }); + + it("should provide jsonlogic for not_between operator", () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const jsonLogic = AttributesBaseConfig.operators.not_between.jsonLogic( + { var: "89ee81ae-953c-409b-aacc-700e1ce5ae20" }, + "not_between", + ["1", "5"] + ); + + // Verifies that it is using < and > in implementation and integer parsed + expect(jsonLogic).toEqual({ + or: [ + { "<": [{ var: "89ee81ae-953c-409b-aacc-700e1ce5ae20" }, 1] }, + { ">": [{ var: "89ee81ae-953c-409b-aacc-700e1ce5ae20" }, 5] }, + ], + }); + }); }); }); diff --git a/packages/app-store/routing-forms/components/react-awesome-query-builder/config/BasicConfig.ts b/packages/app-store/routing-forms/components/react-awesome-query-builder/config/BasicConfig.ts index 86fd692be2..5220118c0f 100644 --- a/packages/app-store/routing-forms/components/react-awesome-query-builder/config/BasicConfig.ts +++ b/packages/app-store/routing-forms/components/react-awesome-query-builder/config/BasicConfig.ts @@ -117,7 +117,13 @@ const operators: Operators = { cardinality: 2, valueLabels: ["Value from", "Value to"], reversedOp: "not_between", - jsonLogic: "<=", + jsonLogic: (field: any, op: any, vals: [any, any]) => { + const min = parseInt(vals[0], 10); + const max = parseInt(vals[1], 10); + return { + and: [{ ">=": [field, min] }, { "<=": [field, max] }], + }; + }, }, not_between: { isNotOp: true, @@ -126,6 +132,13 @@ const operators: Operators = { cardinality: 2, valueLabels: ["Value from", "Value to"], reversedOp: "between", + jsonLogic: (field: any, op: any, vals: [any, any]) => { + const min = parseInt(vals[0], 10); + const max = parseInt(vals[1], 10); + return { + or: [{ "<": [field, min] }, { ">": [field, max] }], + }; + }, }, is_empty: { label: "Is empty", diff --git a/packages/app-store/routing-forms/playwright/tests/basic.e2e.ts b/packages/app-store/routing-forms/playwright/tests/basic.e2e.ts index 0de52e5ded..d0f64a55ee 100644 --- a/packages/app-store/routing-forms/playwright/tests/basic.e2e.ts +++ b/packages/app-store/routing-forms/playwright/tests/basic.e2e.ts @@ -58,6 +58,13 @@ async function selectFirstValueForAttributeValue({ }); } +async function selectOperatorOption({ fromLocator, option }: { fromLocator: Locator; option: number }) { + await selectOptionUsingLocator({ + locator: fromLocator, + option, + }); +} + async function addAttributeRoutingRule(page: Page) { // TODO: Use a better selector maybe? await page.locator('text="Add rule"').nth(1).click(); @@ -75,6 +82,25 @@ async function addAttributeRoutingRule(page: Page) { }); } +async function addAttributeRoutingRuleWithOperator(page: Page, valueFrom: string, valueTo: string) { + await page.locator('text="Add rule"').nth(1).click(); + const attributeQueryBuilder = page.locator(".group-container").nth(1); + const attributeSelectorFirstRule = attributeQueryBuilder.locator(".rule--field").nth(0); + await selectFirstAttributeOption({ + fromLocator: attributeSelectorFirstRule, + }); + + const attributeOperatorSelector = attributeQueryBuilder.locator(".rule--operator").nth(0); + await selectOperatorOption({ + fromLocator: attributeOperatorSelector, + option: 7, + }); + + const attributeValueSelector = attributeQueryBuilder.locator(".rule--value").nth(0); + await attributeValueSelector.locator("input").nth(0).fill(valueFrom); + await attributeValueSelector.locator("input").nth(1).fill(valueTo); +} + async function selectFirstEventRedirectOption(page: Page) { await selectOption({ selector: { @@ -694,6 +720,171 @@ test.describe("Routing Forms", () => { }); }); + test.describe("Form with Attribute Routing with Between operator - Not Matching - Team Form", () => { + test.beforeEach(async ({ page, users }) => { + const userFixture = await users.create( + { username: "routing-forms" }, + { + hasTeam: true, + isOrg: true, + hasSubteam: true, + schedulingType: SchedulingType.ROUND_ROBIN, + } + ); + + const orgMembership = await userFixture.getOrgMembership(); + + const createdAttribute = await prisma.attribute.create({ + data: { + teamId: orgMembership.teamId, + type: AttributeType.NUMBER, + name: "Company Size", + slug: `company-size-orgId-${orgMembership.teamId}`, + options: { + create: [ + { + slug: "10", + value: "10", + }, + ], + }, + }, + include: { + options: true, + }, + }); + + await prisma.attributeToUser.create({ + data: { + member: { + connect: { + userId_teamId: { + userId: userFixture.id, + teamId: orgMembership.teamId, + }, + }, + }, + attributeOption: { + connect: { + id: createdAttribute.options[0].id, + }, + }, + }, + }); + await userFixture.apiLogin(); + }); + + test.afterEach(async ({ users }) => { + // This also delete forms on cascade + await users.deleteAll(); + }); + + test("should not match any member if between operator values are input such that", async ({ page }) => { + await addForm(page, { + forTeam: true, + }); + + await page.locator('[data-testid="toggle-group-item-route-builder"]').nth(1).click(); + await page.waitForURL("/routing/route-builder/**"); + await addNewRoute(page); + // This would select Round Robin event that we created above + await selectFirstEventRedirectOption(page); + await addAttributeRoutingRuleWithOperator(page, "1", "5"); + await saveCurrentForm(page); + + // asserting there is no match as attribute value for the member is 10, while input is between 1 and 5 + await (async function testPreviewWhereThereIsNoMatch() { + await page.click('[data-testid="preview-button"]'); + await page.click('[data-testid="submit-button"]'); + await expect(page.locator('[data-testid="attribute-logic-matched"]')).toHaveText("No"); + await expect(page.locator('[data-testid="attribute-logic-fallback-matched"]')).toHaveText("Yes"); + await page.click('[data-testid="close-results-button"]'); + })(); + }); + }); + + test.describe("Form with Attribute Routing with Between operator - Matching - Team Form", () => { + test.beforeEach(async ({ page, users }) => { + const userFixture = await users.create( + { username: "routing-forms" }, + { + hasTeam: true, + isOrg: true, + hasSubteam: true, + schedulingType: SchedulingType.ROUND_ROBIN, + } + ); + + const orgMembership = await userFixture.getOrgMembership(); + + const createdAttribute = await prisma.attribute.create({ + data: { + teamId: orgMembership.teamId, + type: AttributeType.NUMBER, + name: "Company Size", + slug: `company-size-orgId-${orgMembership.teamId}`, + options: { + create: [ + { + slug: "3", + value: "3", + }, + ], + }, + }, + include: { + options: true, + }, + }); + + await prisma.attributeToUser.create({ + data: { + member: { + connect: { + userId_teamId: { + userId: userFixture.id, + teamId: orgMembership.teamId, + }, + }, + }, + attributeOption: { + connect: { + id: createdAttribute.options[0].id, + }, + }, + }, + }); + await userFixture.apiLogin(); + }); + + test.afterEach(async ({ users }) => { + // This also delete forms on cascade + await users.deleteAll(); + }); + + test("should match the member if between operator values are input such that", async ({ page }) => { + await addForm(page, { + forTeam: true, + }); + + await page.locator('[data-testid="toggle-group-item-route-builder"]').nth(1).click(); + await page.waitForURL("/routing/route-builder/**"); + await addNewRoute(page); + // This would select Round Robin event that we created above + await selectFirstEventRedirectOption(page); + await addAttributeRoutingRuleWithOperator(page, "1", "5"); + await saveCurrentForm(page); + + // asserting there is a match as attribute value for the member is 3 that is between 1 and 5 + await (async function testPreviewWhereThereIsMatch() { + await page.click('[data-testid="preview-button"]'); + await page.click('[data-testid="submit-button"]'); + await page.waitForSelector("text=@example.com"); + await page.click('[data-testid="close-results-button"]'); + })(); + }); + }); + test.describe("Team Routing Form", () => { test.afterEach(async ({ users }) => { await users.deleteAll();