|
| 1 | +import { JSDOM } from "jsdom"; |
| 2 | + |
| 3 | +let computeSimilarityScore; |
| 4 | + |
| 5 | +describe("computeSimilarityScore", () => { |
| 6 | + let chatbot; |
| 7 | + |
| 8 | + beforeEach(async () => { |
| 9 | + const { window } = new JSDOM(`<!DOCTYPE html><body> |
| 10 | + <h1 id="chatbot-name"> </h1> |
| 11 | + <main> |
| 12 | + <div id="chat" class="chat-container" role="region" aria-label="Zone de conversation"> |
| 13 | + <!-- La conversation sera affichée ici --> |
| 14 | + </div> |
| 15 | + <div id="controls"> |
| 16 | + <div id="input-container"> |
| 17 | + <label id="user-input-label" class="sr-only" for="user-input">Écrivez votre message</label> |
| 18 | + <div id="user-input" contenteditable="true" placeholder="Écrivez votre message" tabindex="0" role="textbox" aria-labelledby="user-input-label" title="Écrivez votre message"></div> |
| 19 | + </div> |
| 20 | + <button id="send-button" type="button">Envoyer</button> |
| 21 | + </div> |
| 22 | + </main> |
| 23 | + <footer id="footer"> |
| 24 | + ChatMD – Outil libre & gratuit créé par <a href="https://eyssette.forge.apps.education.fr/">Cédric Eyssette</a> |
| 25 | + </footer> |
| 26 | + <script src="script.min.js"></script> |
| 27 | +</body>`); |
| 28 | + |
| 29 | + global.window = window; |
| 30 | + global.document = window.document; |
| 31 | + |
| 32 | + // Importer le module APRÈS avoir créé global.document |
| 33 | + const mod = await import( |
| 34 | + "../../../../../../app/js/core/interactions/helpers/findBestResponse/computeSimilarityScore.mjs" |
| 35 | + ); |
| 36 | + computeSimilarityScore = mod.computeSimilarityScore; |
| 37 | + |
| 38 | + chatbot = { |
| 39 | + responses: [], |
| 40 | + nextMessage: { |
| 41 | + needsProcessing: false, |
| 42 | + goto: null, |
| 43 | + selected: null, |
| 44 | + ignoreKeywords: false, |
| 45 | + }, |
| 46 | + vectorChatBotResponses: [], |
| 47 | + }; |
| 48 | + }); |
| 49 | + |
| 50 | + it("returns no match when there are no responses", () => { |
| 51 | + const result = computeSimilarityScore(chatbot, "contenu pour tester"); |
| 52 | + |
| 53 | + expect(result.bestMatch).toBe(null); |
| 54 | + expect(result.bestMatchScore).toBe(0); |
| 55 | + expect(result.indexBestMatch).toBeUndefined(); |
| 56 | + }); |
| 57 | + |
| 58 | + it("returns no match when responses have no titles", () => { |
| 59 | + chatbot.responses = [{ content: "Réponse sans titre", title: "" }]; |
| 60 | + const result = computeSimilarityScore(chatbot, "contenu pour tester"); |
| 61 | + |
| 62 | + expect(result.bestMatch).toBe(null); |
| 63 | + expect(result.bestMatchScore).toBe(0); |
| 64 | + }); |
| 65 | + |
| 66 | + it("finds the best matching response based on keywords", () => { |
| 67 | + chatbot.responses = [ |
| 68 | + { |
| 69 | + title: "Horaires", |
| 70 | + keywords: ["horaire", "ouverture", "fermé"], |
| 71 | + content: "Nous sommes ouverts de 9h à 18h", |
| 72 | + }, |
| 73 | + { |
| 74 | + title: "Contact", |
| 75 | + keywords: ["téléphone", "appeler"], |
| 76 | + content: "Voici notre numéro de téléphone : xx-xx-xx-xx-xx", |
| 77 | + }, |
| 78 | + ]; |
| 79 | + const result = computeSimilarityScore( |
| 80 | + chatbot, |
| 81 | + "Comment faire pour vous appeler ?", |
| 82 | + ); |
| 83 | + |
| 84 | + expect(result.bestMatch).toBe( |
| 85 | + "Voici notre numéro de téléphone : xx-xx-xx-xx-xx", |
| 86 | + ); |
| 87 | + expect(result.bestMatchScore).toBeGreaterThan(0); |
| 88 | + expect(result.indexBestMatch).toBe(1); |
| 89 | + }); |
| 90 | + |
| 91 | + it("gives higher score for longer matching keywords", () => { |
| 92 | + chatbot.responses = [ |
| 93 | + { |
| 94 | + title: "Test1", |
| 95 | + keywords: ["info"], |
| 96 | + content: "Réponse courte", |
| 97 | + }, |
| 98 | + { |
| 99 | + title: "Test2", |
| 100 | + keywords: ["information"], |
| 101 | + content: "Réponse longue", |
| 102 | + }, |
| 103 | + { |
| 104 | + title: "Test3", |
| 105 | + keywords: ["informationnel"], |
| 106 | + content: "Réponse encore plus longue", |
| 107 | + }, |
| 108 | + ]; |
| 109 | + |
| 110 | + const result = computeSimilarityScore(chatbot, "information"); |
| 111 | + |
| 112 | + expect(result.indexBestMatch).toBe(1); |
| 113 | + }); |
| 114 | + |
| 115 | + it("handles accents in keywords and user input", () => { |
| 116 | + chatbot.responses = [ |
| 117 | + { |
| 118 | + title: "Info", |
| 119 | + keywords: ["café"], |
| 120 | + content: "Réponse café", |
| 121 | + }, |
| 122 | + ]; |
| 123 | + |
| 124 | + const result = computeSimilarityScore(chatbot, "cafe"); |
| 125 | + |
| 126 | + expect(result.bestMatchScore).toBeGreaterThan(0); |
| 127 | + }); |
| 128 | + |
| 129 | + it("includes title in matching when no keywords", () => { |
| 130 | + chatbot.responses = [ |
| 131 | + { |
| 132 | + title: "Horaires", |
| 133 | + keywords: [], |
| 134 | + content: "Les horaires sont affichés", |
| 135 | + }, |
| 136 | + ]; |
| 137 | + |
| 138 | + const result = computeSimilarityScore(chatbot, "horaires"); |
| 139 | + |
| 140 | + expect(result.bestMatchScore).toBe(30.8); |
| 141 | + }); |
| 142 | + |
| 143 | + it("penalizes responses with negative keywords present in user input", () => { |
| 144 | + chatbot.responses = [ |
| 145 | + { |
| 146 | + title: "Réponse1", |
| 147 | + keywords: ["! non merci", "aide", "aidant", "aidez-moi"], |
| 148 | + content: "Première réponse", |
| 149 | + }, |
| 150 | + { |
| 151 | + title: "Réponse2", |
| 152 | + keywords: ["aide"], |
| 153 | + content: "Deuxième réponse", |
| 154 | + }, |
| 155 | + ]; |
| 156 | + |
| 157 | + const result = computeSimilarityScore(chatbot, "aide non merci"); |
| 158 | + |
| 159 | + expect(result.indexBestMatch).toBe(1); |
| 160 | + }); |
| 161 | + |
| 162 | + it("goes to the specified response with !Next", () => { |
| 163 | + chatbot.responses = [ |
| 164 | + { |
| 165 | + title: "Etape1", |
| 166 | + keywords: [], |
| 167 | + content: "Première étape", |
| 168 | + }, |
| 169 | + { |
| 170 | + title: "Etape2", |
| 171 | + keywords: [], |
| 172 | + content: "Deuxième étape", |
| 173 | + }, |
| 174 | + ]; |
| 175 | + chatbot.nextMessage.needsProcessing = true; |
| 176 | + chatbot.nextMessage.goto = "Etape2"; |
| 177 | + |
| 178 | + const result = computeSimilarityScore(chatbot, "contenu pour tester"); |
| 179 | + |
| 180 | + expect(result.indexBestMatch).toBe(1); |
| 181 | + }); |
| 182 | + |
| 183 | + it("prioritizes the !Next response even if keywords match another response better", () => { |
| 184 | + chatbot.responses = [ |
| 185 | + { |
| 186 | + title: "Autre", |
| 187 | + keywords: ["argument", "argumentation", "argumenter", "arguments"], |
| 188 | + content: "Autre réponse", |
| 189 | + }, |
| 190 | + { |
| 191 | + title: "Cible", |
| 192 | + keywords: ["argument"], |
| 193 | + content: "Réponse ciblée", |
| 194 | + }, |
| 195 | + ]; |
| 196 | + chatbot.nextMessage.needsProcessing = true; |
| 197 | + chatbot.nextMessage.goto = "Cible"; |
| 198 | + |
| 199 | + const result = computeSimilarityScore(chatbot, "argument"); |
| 200 | + |
| 201 | + expect(result.indexBestMatch).toBe(1); |
| 202 | + }); |
| 203 | + |
| 204 | + it("takes keywords into account for !Next if ignoreKeywords is set to false", () => { |
| 205 | + chatbot.responses = [ |
| 206 | + { |
| 207 | + title: "Cible", |
| 208 | + keywords: ["specifique"], |
| 209 | + content: "Réponse avec keyword", |
| 210 | + }, |
| 211 | + ]; |
| 212 | + chatbot.nextMessage.needsProcessing = true; |
| 213 | + chatbot.nextMessage.goto = "Cible"; |
| 214 | + chatbot.nextMessage.ignoreKeywords = false; |
| 215 | + |
| 216 | + const resultWithKeyword = computeSimilarityScore(chatbot, "specifique"); |
| 217 | + const resultWithoutKeyword = computeSimilarityScore(chatbot, "autre"); |
| 218 | + |
| 219 | + expect(resultWithKeyword.bestMatchScore).toBeGreaterThan( |
| 220 | + resultWithoutKeyword.bestMatchScore, |
| 221 | + ); |
| 222 | + expect(resultWithKeyword.indexBestMatch).toBe(0); |
| 223 | + expect(resultWithoutKeyword.indexBestMatch).not.toBe(0); |
| 224 | + }); |
| 225 | + |
| 226 | + it("gives a higher score for exact match and a lower score for partial match in !Next mode", () => { |
| 227 | + chatbot.responses = [ |
| 228 | + { |
| 229 | + title: "contenu pour tester", |
| 230 | + keywords: ["contenu pour tester"], |
| 231 | + content: "Réponse test", |
| 232 | + }, |
| 233 | + ]; |
| 234 | + chatbot.nextMessage.needsProcessing = true; |
| 235 | + chatbot.nextMessage.goto = "contenu pour tester"; |
| 236 | + |
| 237 | + const resultExact = computeSimilarityScore(chatbot, "contenu pour tester"); |
| 238 | + const resultPartiel = computeSimilarityScore(chatbot, "testabc"); |
| 239 | + |
| 240 | + expect(resultExact.bestMatchScore).toBeGreaterThan( |
| 241 | + resultPartiel.bestMatchScore, |
| 242 | + ); |
| 243 | + }); |
| 244 | + |
| 245 | + it("checks only the selected response with !SelectNext", () => { |
| 246 | + chatbot.responses = [ |
| 247 | + { |
| 248 | + title: "Option1", |
| 249 | + keywords: ["test", "tests", "tester", "testons"], |
| 250 | + content: "Première option", |
| 251 | + }, |
| 252 | + { |
| 253 | + title: "Option2", |
| 254 | + keywords: ["test"], |
| 255 | + content: "Deuxième option", |
| 256 | + }, |
| 257 | + ]; |
| 258 | + chatbot.nextMessage.selected = "Option2"; |
| 259 | + |
| 260 | + const result = computeSimilarityScore(chatbot, "testons"); |
| 261 | + |
| 262 | + expect(result.indexBestMatch).toBe(1); |
| 263 | + }); |
| 264 | + |
| 265 | + it("finds approximate matches with minor typos", () => { |
| 266 | + chatbot.responses = [ |
| 267 | + { |
| 268 | + title: "contenu pour tester", |
| 269 | + keywords: ["bonjour"], |
| 270 | + content: "Réponse bonjour", |
| 271 | + }, |
| 272 | + ]; |
| 273 | + |
| 274 | + const result = computeSimilarityScore(chatbot, "bonjur"); |
| 275 | + |
| 276 | + expect(result.bestMatchScore).toBeGreaterThan(0); |
| 277 | + }); |
| 278 | + |
| 279 | + it("ignores or penalizes greatly short keywords in user input", () => { |
| 280 | + chatbot.responses = [ |
| 281 | + { |
| 282 | + title: "contenu pour tester", |
| 283 | + keywords: ["bonjour", "poteau"], |
| 284 | + content: "Réponse", |
| 285 | + }, |
| 286 | + ]; |
| 287 | + |
| 288 | + const result = computeSimilarityScore(chatbot, "bon pote"); |
| 289 | + |
| 290 | + expect(result.bestMatchScore).toBeLessThan(5); |
| 291 | + }); |
| 292 | + |
| 293 | + it("does not ignore short keywords", () => { |
| 294 | + chatbot.responses = [ |
| 295 | + { |
| 296 | + title: "contenu pour tester", |
| 297 | + keywords: ["art"], |
| 298 | + content: "Réponse courte", |
| 299 | + }, |
| 300 | + ]; |
| 301 | + |
| 302 | + const result = computeSimilarityScore( |
| 303 | + chatbot, |
| 304 | + "j'aime ce qui est artistique", |
| 305 | + ); |
| 306 | + |
| 307 | + expect(result.bestMatchScore).toBe(30.3); |
| 308 | + }); |
| 309 | + |
| 310 | + it("matches keywords regardless of case", () => { |
| 311 | + chatbot.responses = [ |
| 312 | + { |
| 313 | + title: "contenu pour tester", |
| 314 | + keywords: ["BOnJoUR"], |
| 315 | + content: "Salut", |
| 316 | + }, |
| 317 | + ]; |
| 318 | + |
| 319 | + const result = computeSimilarityScore(chatbot, "bonjour"); |
| 320 | + |
| 321 | + expect(result.bestMatchScore).toBeGreaterThan(0); |
| 322 | + }); |
| 323 | + |
| 324 | + it("selects the response with the highest bestMatchScore", () => { |
| 325 | + chatbot.responses = [ |
| 326 | + { |
| 327 | + title: "Faible", |
| 328 | + keywords: ["correspondance", "moins", "importante"], |
| 329 | + content: "Réponse faible", |
| 330 | + }, |
| 331 | + { |
| 332 | + title: "Forte", |
| 333 | + keywords: ["correspondance", "plus", "importante"], |
| 334 | + content: "Réponse forte", |
| 335 | + }, |
| 336 | + ]; |
| 337 | + |
| 338 | + const result = computeSimilarityScore( |
| 339 | + chatbot, |
| 340 | + "correspondance plus importante", |
| 341 | + ); |
| 342 | + |
| 343 | + expect(result.indexBestMatch).toBe(1); |
| 344 | + expect(result.bestMatch).toBe("Réponse forte"); |
| 345 | + }); |
| 346 | +}); |
0 commit comments