From 646aecece123fe0194a5eb346965b07c1ee3bda7 Mon Sep 17 00:00:00 2001 From: "Abdullah." <125115953+mabdullahabaid@users.noreply.github.com> Date: Sun, 21 Sep 2025 11:47:14 +0500 Subject: [PATCH] Update integration test to include cases for bi-directional accent-insensitive search. (#14631) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Enhanced the search resolver integration tests to comprehensively validate bidirectional accent-insensitive search functionality. Users can now search with or without accents and find all relevant records regardless of how the data was originally stored. ## Changes Made ### Test Data Enhancements - Added 2 new person IDs: `TEST_PERSON_6_ID`, `TEST_PERSON_7_ID` - Expanded corpus from 5 to 13 records: - 7 persons - 2 companies - 4 pets - Refactored test data into single arrays with content-driven variable naming for better maintainability ### Test Coverage Improvements - Added 6 comprehensive bidirectional tests covering French, Spanish, and German characters - Validated multiple fields: - `firstName`, `lastName`, `jobTitle`, `email`, `company name`, `pet name` - Cross-object search testing across Person, Company, and Pet - Updated all ranking values to account for the enlarged test corpus (5 → 13 records) ## Test Cases Added/Updated - [x] should find both **"José"** and **"Jose"** when searching for `"jose"` (bidirectional accent-insensitive) - [x] should find both **"García"** and **"Garcia"** when searching for `"garcia"` (bidirectional accent-insensitive) - [x] should find both accented and non-accented **"Café"** / **"Cafe"** records when searching for `"cafe"` (bidirectional accent-insensitive) - [x] should find both accented and non-accented **"Naïve"** / **"Naive"** records when searching for `"naive"` (bidirectional accent-insensitive) - [x] should find both **"Müller"** and **"Muller"** when searching for `"muller"` (bidirectional accent-insensitive) - [x] should find both **"François"** and **"Francois"** when searching for `"francois"` (bidirectional accent-insensitive) --- .../constants/test-person-ids.constants.ts | 2 + .../constants/test-pet-ids.constants.ts | 2 + .../search-resolver.integration-spec.ts | 370 ++++++++++++++---- 3 files changed, 306 insertions(+), 68 deletions(-) diff --git a/packages/twenty-server/test/integration/constants/test-person-ids.constants.ts b/packages/twenty-server/test/integration/constants/test-person-ids.constants.ts index bf38621b78..b79fdea2c9 100644 --- a/packages/twenty-server/test/integration/constants/test-person-ids.constants.ts +++ b/packages/twenty-server/test/integration/constants/test-person-ids.constants.ts @@ -3,5 +3,7 @@ export const TEST_PERSON_2_ID = '777a8457-eb2d-40ac-a707-551b615b6981'; export const TEST_PERSON_3_ID = '777a8457-eb2d-40ac-a707-551b615b6982'; export const TEST_PERSON_4_ID = '777a8457-eb2d-40ac-a707-551b615b6983'; export const TEST_PERSON_5_ID = '777a8457-eb2d-40ac-a707-551b615b6984'; +export const TEST_PERSON_6_ID = '777a8457-eb2d-40ac-a707-551b615b6985'; +export const TEST_PERSON_7_ID = '777a8457-eb2d-40ac-a707-551b615b6986'; export const NOT_EXISTING_TEST_PERSON_ID = '777a8457-eb2d-40ac-a707-551b615b6990'; diff --git a/packages/twenty-server/test/integration/constants/test-pet-ids.constants.ts b/packages/twenty-server/test/integration/constants/test-pet-ids.constants.ts index b368761e25..0cadfb85f5 100644 --- a/packages/twenty-server/test/integration/constants/test-pet-ids.constants.ts +++ b/packages/twenty-server/test/integration/constants/test-pet-ids.constants.ts @@ -1,2 +1,4 @@ export const TEST_PET_ID_1 = 'a4907cff-a582-4daf-8635-ad6c782c7c25'; export const TEST_PET_ID_2 = 'c4e97187-9b9b-4e1f-a3c5-b7883c590332'; +export const TEST_PET_ID_3 = 'e4907cff-a582-4aaf-8635-ad6c782c7c26'; +export const TEST_PET_ID_4 = 'f4907cff-a582-4aaf-8635-ad6c782c7c27'; diff --git a/packages/twenty-server/test/integration/graphql/suites/search/search-resolver.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/search/search-resolver.integration-spec.ts index 1a782de2c3..1bea4a0c1a 100644 --- a/packages/twenty-server/test/integration/graphql/suites/search/search-resolver.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/search/search-resolver.integration-spec.ts @@ -1,13 +1,24 @@ +import { COMPANY_GQL_FIELDS } from 'test/integration/constants/company-gql-fields.constants'; import { OBJECT_MODEL_COMMON_FIELDS } from 'test/integration/constants/object-model-common-fields'; import { PERSON_GQL_FIELDS } from 'test/integration/constants/person-gql-fields.constants'; +import { + TEST_COMPANY_1_ID, + TEST_COMPANY_2_ID, +} from 'test/integration/constants/test-company-ids.constants'; import { TEST_PERSON_1_ID, TEST_PERSON_2_ID, TEST_PERSON_3_ID, + TEST_PERSON_4_ID, + TEST_PERSON_5_ID, + TEST_PERSON_6_ID, + TEST_PERSON_7_ID, } from 'test/integration/constants/test-person-ids.constants'; import { TEST_PET_ID_1, TEST_PET_ID_2, + TEST_PET_ID_3, + TEST_PET_ID_4, } from 'test/integration/constants/test-pet-ids.constants'; import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; import { performCreateManyOperation } from 'test/integration/graphql/utils/perform-create-many-operation.utils'; @@ -24,17 +35,60 @@ import { type SearchResultEdgeDTO } from 'src/engine/core-modules/search/dtos/se import { type SearchCursor } from 'src/engine/core-modules/search/services/search.service'; describe('SearchResolver', () => { - const [firstPerson, secondPerson, thirdPerson] = [ + const persons = [ { id: TEST_PERSON_1_ID, name: { firstName: 'searchInput1' } }, { id: TEST_PERSON_2_ID, name: { firstName: 'searchInput2' } }, { id: TEST_PERSON_3_ID, name: { firstName: 'searchInput3' } }, + { + id: TEST_PERSON_4_ID, + name: { firstName: 'José', lastName: 'García' }, + jobTitle: 'Café Manager', + emails: { primaryEmail: 'josé@café.com' }, + }, + { + id: TEST_PERSON_5_ID, + name: { firstName: 'François', lastName: 'Müller' }, + jobTitle: 'Manager', + emails: { primaryEmail: 'françois@naïve.com' }, + }, + { + id: TEST_PERSON_6_ID, + name: { firstName: 'Jose', lastName: 'Garcia' }, + jobTitle: 'Cafe Manager', + emails: { primaryEmail: 'jose@cafe.com' }, + }, + { + id: TEST_PERSON_7_ID, + name: { firstName: 'Francois', lastName: 'Muller' }, + jobTitle: 'Manager', + emails: { primaryEmail: 'francois@naive.com' }, + }, ]; - const [firstPet, secondPet] = [ + const companies = [ + { id: TEST_COMPANY_1_ID, name: 'Café Corp' }, + { id: TEST_COMPANY_2_ID, name: 'Naïve Solutions' }, + ]; + + const pets = [ { id: TEST_PET_ID_1, name: 'searchInput1' }, { id: TEST_PET_ID_2, name: 'searchInput2' }, + { id: TEST_PET_ID_3, name: 'Café' }, + { id: TEST_PET_ID_4, name: 'Naïve' }, ]; + const [ + searchInput1Person, + searchInput2Person, + searchInput3Person, + josePerson, + francoisPerson, + josePersonNoAccent, + francoisPersonNoAccent, + ] = persons; + const [cafeCorp, naiveCorp] = companies; + const [searchInput1Pet, searchInput2Pet, cafePet, naivePet] = pets; + beforeAll(async () => { await deleteAllRecords('person'); await deleteAllRecords('company'); @@ -52,14 +106,22 @@ describe('SearchResolver', () => { 'pet', 'pets', OBJECT_MODEL_COMMON_FIELDS, - [firstPet, secondPet], + pets, ); - await performCreateManyOperation('person', 'people', PERSON_GQL_FIELDS, [ - firstPerson, - secondPerson, - thirdPerson, - ]); + await performCreateManyOperation( + 'person', + 'people', + PERSON_GQL_FIELDS, + persons, + ); + + await performCreateManyOperation( + 'company', + 'companies', + COMPANY_GQL_FIELDS, + companies, + ); } catch (error) { // eslint-disable-next-line no-console console.log(error); @@ -88,19 +150,28 @@ describe('SearchResolver', () => { }, eval: { orderedRecordIds: [ - firstPerson.id, - secondPerson.id, - thirdPerson.id, - firstPet.id, - secondPet.id, + searchInput1Person.id, + searchInput2Person.id, + searchInput3Person.id, + josePerson.id, + francoisPerson.id, + josePersonNoAccent.id, + francoisPersonNoAccent.id, + naiveCorp.id, + cafeCorp.id, + searchInput1Pet.id, + searchInput2Pet.id, + cafePet.id, + naivePet.id, ], pageInfo: { hasNextPage: false, decodedEndCursor: { lastRanks: { tsRank: 0, tsRankCD: 0 }, lastRecordIdsPerObject: { - person: thirdPerson.id, - pet: secondPet.id, + person: francoisPersonNoAccent.id, + company: cafeCorp.id, + pet: naivePet.id, }, }, }, @@ -116,14 +187,14 @@ describe('SearchResolver', () => { limit: 50, }, eval: { - orderedRecordIds: [firstPerson.id, firstPet.id], + orderedRecordIds: [searchInput1Person.id, searchInput1Pet.id], pageInfo: { hasNextPage: false, decodedEndCursor: { lastRanks: { tsRank: 0.06079271, tsRankCD: 0.1 }, lastRecordIdsPerObject: { - person: firstPerson.id, - pet: firstPet.id, + person: searchInput1Person.id, + pet: searchInput1Pet.id, }, }, }, @@ -140,13 +211,18 @@ describe('SearchResolver', () => { limit: 50, }, eval: { - orderedRecordIds: [firstPet.id, secondPet.id], + orderedRecordIds: [ + searchInput1Pet.id, + searchInput2Pet.id, + cafePet.id, + naivePet.id, + ], pageInfo: { hasNextPage: false, decodedEndCursor: { lastRanks: { tsRank: 0, tsRankCD: 0 }, lastRecordIdsPerObject: { - pet: secondPet.id, + pet: naivePet.id, }, }, }, @@ -162,13 +238,21 @@ describe('SearchResolver', () => { limit: 50, }, eval: { - orderedRecordIds: [firstPet.id, secondPet.id], + orderedRecordIds: [ + naiveCorp.id, + cafeCorp.id, + searchInput1Pet.id, + searchInput2Pet.id, + cafePet.id, + naivePet.id, + ], pageInfo: { hasNextPage: false, decodedEndCursor: { lastRanks: { tsRank: 0, tsRankCD: 0 }, lastRecordIdsPerObject: { - pet: secondPet.id, + company: cafeCorp.id, + pet: naivePet.id, }, }, }, @@ -181,17 +265,17 @@ describe('SearchResolver', () => { input: { searchInput: '', excludedObjectNameSingulars: ['workspaceMember'], - filter: { id: { eq: firstPet.id } }, + filter: { id: { eq: searchInput1Pet.id } }, limit: 50, }, eval: { - orderedRecordIds: [firstPet.id], + orderedRecordIds: [searchInput1Pet.id], pageInfo: { hasNextPage: false, decodedEndCursor: { lastRanks: { tsRank: 0, tsRankCD: 0 }, lastRecordIdsPerObject: { - pet: firstPet.id, + pet: searchInput1Pet.id, }, }, }, @@ -208,18 +292,17 @@ describe('SearchResolver', () => { }, eval: { orderedRecordIds: [ - firstPerson.id, - secondPerson.id, - thirdPerson.id, - firstPet.id, + searchInput1Person.id, + searchInput2Person.id, + searchInput3Person.id, + josePerson.id, ], pageInfo: { hasNextPage: true, decodedEndCursor: { lastRanks: { tsRank: 0, tsRankCD: 0 }, lastRecordIdsPerObject: { - pet: firstPet.id, - person: thirdPerson.id, + person: josePerson.id, }, }, }, @@ -235,13 +318,13 @@ describe('SearchResolver', () => { limit: 2, }, eval: { - orderedRecordIds: [firstPerson.id, secondPerson.id], + orderedRecordIds: [searchInput1Person.id, searchInput2Person.id], pageInfo: { hasNextPage: true, decodedEndCursor: { lastRanks: { tsRank: 0, tsRankCD: 0 }, lastRecordIdsPerObject: { - person: secondPerson.id, + person: searchInput2Person.id, }, }, }, @@ -257,20 +340,19 @@ describe('SearchResolver', () => { after: encodeCursorData({ lastRanks: { tsRank: 0, tsRankCD: 0 }, lastRecordIdsPerObject: { - person: secondPerson.id, + person: searchInput2Person.id, }, }), limit: 2, }, eval: { - orderedRecordIds: [thirdPerson.id, firstPet.id], + orderedRecordIds: [searchInput3Person.id, josePerson.id], pageInfo: { hasNextPage: true, decodedEndCursor: { lastRanks: { tsRank: 0, tsRankCD: 0 }, lastRecordIdsPerObject: { - pet: firstPet.id, - person: thirdPerson.id, + person: josePerson.id, }, }, }, @@ -287,18 +369,18 @@ describe('SearchResolver', () => { }, eval: { orderedRecordIds: [ - firstPerson.id, - secondPerson.id, - thirdPerson.id, - firstPet.id, + searchInput1Person.id, + searchInput2Person.id, + searchInput3Person.id, + searchInput1Pet.id, ], pageInfo: { hasNextPage: true, decodedEndCursor: { lastRanks: { tsRank: 0.06079271, tsRankCD: 0.1 }, lastRecordIdsPerObject: { - pet: firstPet.id, - person: thirdPerson.id, + pet: searchInput1Pet.id, + person: searchInput3Person.id, }, }, }, @@ -314,13 +396,13 @@ describe('SearchResolver', () => { limit: 2, }, eval: { - orderedRecordIds: [firstPerson.id, secondPerson.id], + orderedRecordIds: [searchInput1Person.id, searchInput2Person.id], pageInfo: { hasNextPage: true, decodedEndCursor: { lastRanks: { tsRank: 0.06079271, tsRankCD: 0.1 }, lastRecordIdsPerObject: { - person: secondPerson.id, + person: searchInput2Person.id, }, }, }, @@ -337,20 +419,20 @@ describe('SearchResolver', () => { after: encodeCursorData({ lastRanks: { tsRank: 0.06079271, tsRankCD: 0.1 }, lastRecordIdsPerObject: { - person: secondPerson.id, + person: searchInput2Person.id, }, }), limit: 2, }, eval: { - orderedRecordIds: [thirdPerson.id, firstPet.id], + orderedRecordIds: [searchInput3Person.id, searchInput1Pet.id], pageInfo: { hasNextPage: true, decodedEndCursor: { lastRanks: { tsRank: 0.06079271, tsRankCD: 0.1 }, lastRecordIdsPerObject: { - pet: firstPet.id, - person: thirdPerson.id, + pet: searchInput1Pet.id, + person: searchInput3Person.id, }, }, }, @@ -367,21 +449,21 @@ describe('SearchResolver', () => { after: encodeCursorData({ lastRanks: { tsRank: 0.06079271, tsRankCD: 0.1 }, lastRecordIdsPerObject: { - person: secondPerson.id, + person: searchInput2Person.id, }, }), limit: 2, - filter: { id: { neq: firstPet.id } }, + filter: { id: { neq: searchInput1Pet.id } }, }, eval: { - orderedRecordIds: [thirdPerson.id, secondPet.id], + orderedRecordIds: [searchInput3Person.id, searchInput2Pet.id], pageInfo: { hasNextPage: false, decodedEndCursor: { lastRanks: { tsRank: 0.06079271, tsRankCD: 0.1 }, lastRecordIdsPerObject: { - person: thirdPerson.id, - pet: secondPet.id, + person: searchInput3Person.id, + pet: searchInput2Pet.id, }, }, }, @@ -397,13 +479,13 @@ describe('SearchResolver', () => { limit: 1, }, eval: { - orderedRecordIds: [firstPet.id], + orderedRecordIds: [naiveCorp.id], pageInfo: { hasNextPage: true, decodedEndCursor: { lastRanks: { tsRank: 0, tsRankCD: 0 }, lastRecordIdsPerObject: { - pet: firstPet.id, + company: naiveCorp.id, }, }, }, @@ -420,13 +502,13 @@ describe('SearchResolver', () => { limit: 1, }, eval: { - orderedRecordIds: [firstPet.id], + orderedRecordIds: [searchInput1Pet.id], pageInfo: { hasNextPage: true, decodedEndCursor: { lastRanks: { tsRank: 0, tsRankCD: 0 }, lastRecordIdsPerObject: { - pet: firstPet.id, + pet: searchInput1Pet.id, }, }, }, @@ -450,6 +532,158 @@ describe('SearchResolver', () => { }, }, }, + { + title: + 'should find both "José" and "Jose" when searching for "jose" (bidirectional accent-insensitive)', + context: { + input: { + searchInput: 'jose', + excludedObjectNameSingulars: ['workspaceMember'], + limit: 50, + }, + eval: { + orderedRecordIds: [josePerson.id, josePersonNoAccent.id], + pageInfo: { + hasNextPage: false, + decodedEndCursor: { + lastRanks: { tsRank: 0.12158542, tsRankCD: 0.2 }, + lastRecordIdsPerObject: { + person: josePersonNoAccent.id, + }, + }, + }, + }, + }, + }, + { + title: + 'should find both "García" and "Garcia" when searching for "garcia" (bidirectional accent-insensitive)', + context: { + input: { + searchInput: 'garcia', + excludedObjectNameSingulars: ['workspaceMember'], + limit: 50, + }, + eval: { + orderedRecordIds: [josePerson.id, josePersonNoAccent.id], + pageInfo: { + hasNextPage: false, + decodedEndCursor: { + lastRanks: { tsRank: 0.06079271, tsRankCD: 0.1 }, + lastRecordIdsPerObject: { + person: josePersonNoAccent.id, + }, + }, + }, + }, + }, + }, + { + title: + 'should find both accented and non-accented "Café"/"Cafe" records when searching for "cafe" (bidirectional accent-insensitive)', + context: { + input: { + searchInput: 'cafe', + excludedObjectNameSingulars: ['workspaceMember'], + limit: 50, + }, + eval: { + orderedRecordIds: [ + josePerson.id, + josePersonNoAccent.id, + cafeCorp.id, + cafePet.id, + ], + pageInfo: { + hasNextPage: false, + decodedEndCursor: { + lastRanks: { tsRank: 0.06079271, tsRankCD: 0.1 }, + lastRecordIdsPerObject: { + person: josePersonNoAccent.id, + company: cafeCorp.id, + pet: cafePet.id, + }, + }, + }, + }, + }, + }, + { + title: + 'should find both accented and non-accented "Naïve"/"Naive" records when searching for "naive" (bidirectional accent-insensitive)', + context: { + input: { + searchInput: 'naive', + excludedObjectNameSingulars: ['workspaceMember'], + limit: 50, + }, + eval: { + orderedRecordIds: [ + francoisPerson.id, + francoisPersonNoAccent.id, + naiveCorp.id, + naivePet.id, + ], + pageInfo: { + hasNextPage: false, + decodedEndCursor: { + lastRanks: { tsRank: 0.06079271, tsRankCD: 0.1 }, + lastRecordIdsPerObject: { + person: francoisPersonNoAccent.id, + company: naiveCorp.id, + pet: naivePet.id, + }, + }, + }, + }, + }, + }, + { + title: + 'should find both "Müller" and "Muller" when searching for "muller" (bidirectional accent-insensitive)', + context: { + input: { + searchInput: 'muller', + excludedObjectNameSingulars: ['workspaceMember'], + limit: 50, + }, + eval: { + orderedRecordIds: [francoisPerson.id, francoisPersonNoAccent.id], + pageInfo: { + hasNextPage: false, + decodedEndCursor: { + lastRanks: { tsRank: 0.06079271, tsRankCD: 0.1 }, + lastRecordIdsPerObject: { + person: francoisPersonNoAccent.id, + }, + }, + }, + }, + }, + }, + { + title: + 'should find both "François" and "Francois" when searching for "francois" (bidirectional accent-insensitive)', + context: { + input: { + searchInput: 'francois', + excludedObjectNameSingulars: ['workspaceMember'], + limit: 50, + }, + eval: { + orderedRecordIds: [francoisPerson.id, francoisPersonNoAccent.id], + pageInfo: { + hasNextPage: false, + decodedEndCursor: { + lastRanks: { tsRank: 0.12158542, tsRankCD: 0.2 }, + lastRecordIdsPerObject: { + person: francoisPersonNoAccent.id, + }, + }, + }, + }, + }, + }, ]; it.each(testsUseCases)('$title', async ({ context }) => { @@ -497,7 +731,7 @@ describe('SearchResolver', () => { cursor: encodeCursorData({ lastRanks: { tsRankCD: 0.1, tsRank: 0.06079271 }, lastRecordIdsPerObject: { - person: firstPerson.id, + person: searchInput1Person.id, }, }), }, @@ -505,7 +739,7 @@ describe('SearchResolver', () => { cursor: encodeCursorData({ lastRanks: { tsRankCD: 0.1, tsRank: 0.06079271 }, lastRecordIdsPerObject: { - person: secondPerson.id, + person: searchInput2Person.id, }, }), }, @@ -515,7 +749,7 @@ describe('SearchResolver', () => { endCursor: encodeCursorData({ lastRanks: { tsRankCD: 0.1, tsRank: 0.06079271 }, lastRecordIdsPerObject: { - person: secondPerson.id, + person: searchInput2Person.id, }, }), }, @@ -537,9 +771,9 @@ describe('SearchResolver', () => { excludedObjectNameSingulars: ['workspaceMember'], limit: 2, after: encodeCursorData({ - lastRanks: { tsRankCD: 0.1, tsRank: 0.06079271 }, + lastRanks: { tsRank: 0.06079271, tsRankCD: 0.1 }, lastRecordIdsPerObject: { - person: secondPerson.id, + person: searchInput2Person.id, }, }), }); @@ -552,7 +786,7 @@ describe('SearchResolver', () => { cursor: encodeCursorData({ lastRanks: { tsRankCD: 0.1, tsRank: 0.06079271 }, lastRecordIdsPerObject: { - person: thirdPerson.id, + person: searchInput3Person.id, }, }), }, @@ -560,8 +794,8 @@ describe('SearchResolver', () => { cursor: encodeCursorData({ lastRanks: { tsRankCD: 0.1, tsRank: 0.06079271 }, lastRecordIdsPerObject: { - person: thirdPerson.id, - pet: firstPet.id, + person: searchInput3Person.id, + pet: searchInput1Pet.id, }, }), }, @@ -571,8 +805,8 @@ describe('SearchResolver', () => { endCursor: encodeCursorData({ lastRanks: { tsRankCD: 0.1, tsRank: 0.06079271 }, lastRecordIdsPerObject: { - person: thirdPerson.id, - pet: firstPet.id, + person: searchInput3Person.id, + pet: searchInput1Pet.id, }, }), },