👁️
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

at dev 857 lines 25 kB view raw
1import { beforeAll, describe, expect, it } from "vitest"; 2import { 3 setupTestCards, 4 type TestCardLookup, 5} from "../../__tests__/test-card-lookup"; 6import type { Card } from "../../scryfall-types"; 7import { search } from "../index"; 8 9describe("Scryfall search integration", () => { 10 let cards: TestCardLookup; 11 12 beforeAll(async () => { 13 cards = await setupTestCards(); 14 }, 30_000); 15 16 describe("name matching", () => { 17 it("matches bare word against name", async () => { 18 const bolt = await cards.get("Lightning Bolt"); 19 const result = search("bolt"); 20 expect(result.ok).toBe(true); 21 if (result.ok) { 22 expect(result.value.match(bolt)).toBe(true); 23 } 24 }); 25 26 it("matches quoted phrase", async () => { 27 const bolt = await cards.get("Lightning Bolt"); 28 const result = search('"Lightning Bolt"'); 29 expect(result.ok).toBe(true); 30 if (result.ok) { 31 expect(result.value.match(bolt)).toBe(true); 32 } 33 }); 34 35 it("matches exact name with !", async () => { 36 const bolt = await cards.get("Lightning Bolt"); 37 const result = search('!"Lightning Bolt"'); 38 expect(result.ok).toBe(true); 39 if (result.ok) { 40 expect(result.value.match(bolt)).toBe(true); 41 } 42 43 // Partial shouldn't match 44 const partial = search("!Lightning"); 45 expect(partial.ok).toBe(true); 46 if (partial.ok) { 47 expect(partial.value.match(bolt)).toBe(false); 48 } 49 }); 50 51 it("matches regex against name", async () => { 52 const bolt = await cards.get("Lightning Bolt"); 53 const result = search("/^lightning/i"); 54 expect(result.ok).toBe(true); 55 if (result.ok) { 56 expect(result.value.match(bolt)).toBe(true); 57 } 58 }); 59 60 it("matches names with diacritics using ASCII equivalents", async () => { 61 const nazgul = await cards.get("Nazgûl"); 62 63 // Should match without the diacritic 64 const withoutDiacritic = search("nazgul"); 65 expect(withoutDiacritic.ok).toBe(true); 66 if (withoutDiacritic.ok) { 67 expect(withoutDiacritic.value.match(nazgul)).toBe(true); 68 } 69 70 // Should also match with the diacritic 71 const withDiacritic = search("nazgûl"); 72 expect(withDiacritic.ok).toBe(true); 73 if (withDiacritic.ok) { 74 expect(withDiacritic.value.match(nazgul)).toBe(true); 75 } 76 }); 77 78 it("exact name match works with diacritics", async () => { 79 const nazgul = await cards.get("Nazgûl"); 80 81 // ASCII equivalent should match exactly 82 const ascii = search('!"Nazgul"'); 83 expect(ascii.ok).toBe(true); 84 if (ascii.ok) { 85 expect(ascii.value.match(nazgul)).toBe(true); 86 } 87 88 // With diacritic should also match 89 const diacritic = search('!"Nazgûl"'); 90 expect(diacritic.ok).toBe(true); 91 if (diacritic.ok) { 92 expect(diacritic.value.match(nazgul)).toBe(true); 93 } 94 }); 95 }); 96 97 describe("type matching", () => { 98 it("t: matches type line", async () => { 99 const bolt = await cards.get("Lightning Bolt"); 100 const elves = await cards.get("Llanowar Elves"); 101 102 const instant = search("t:instant"); 103 expect(instant.ok).toBe(true); 104 if (instant.ok) { 105 expect(instant.value.match(bolt)).toBe(true); 106 expect(instant.value.match(elves)).toBe(false); 107 } 108 109 const creature = search("t:creature"); 110 expect(creature.ok).toBe(true); 111 if (creature.ok) { 112 expect(creature.value.match(elves)).toBe(true); 113 expect(creature.value.match(bolt)).toBe(false); 114 } 115 }); 116 117 it("t: matches subtypes", async () => { 118 const elves = await cards.get("Llanowar Elves"); 119 const result = search("t:elf"); 120 expect(result.ok).toBe(true); 121 if (result.ok) { 122 expect(result.value.match(elves)).toBe(true); 123 } 124 }); 125 }); 126 127 describe("oracle text matching", () => { 128 it("o: matches oracle text", async () => { 129 const bolt = await cards.get("Lightning Bolt"); 130 const result = search("o:damage"); 131 expect(result.ok).toBe(true); 132 if (result.ok) { 133 expect(result.value.match(bolt)).toBe(true); 134 } 135 }); 136 137 it("o: with regex", async () => { 138 const bolt = await cards.get("Lightning Bolt"); 139 const result = search("o:/deals? \\d+ damage/i"); 140 expect(result.ok).toBe(true); 141 if (result.ok) { 142 expect(result.value.match(bolt)).toBe(true); 143 } 144 }); 145 }); 146 147 describe("color matching", () => { 148 it("c: matches colors", async () => { 149 const bolt = await cards.get("Lightning Bolt"); 150 const result = search("c:r"); 151 expect(result.ok).toBe(true); 152 if (result.ok) { 153 expect(result.value.match(bolt)).toBe(true); 154 } 155 }); 156 157 it("c= matches exact colors", async () => { 158 const bolt = await cards.get("Lightning Bolt"); 159 const exact = search("c=r"); 160 expect(exact.ok).toBe(true); 161 if (exact.ok) { 162 expect(exact.value.match(bolt)).toBe(true); 163 } 164 165 // Bolt shouldn't match multicolor 166 const multi = search("c=rg"); 167 expect(multi.ok).toBe(true); 168 if (multi.ok) { 169 expect(multi.value.match(bolt)).toBe(false); 170 } 171 }); 172 173 it("c!= excludes exact color", async () => { 174 const bolt = await cards.get("Lightning Bolt"); 175 const elves = await cards.get("Llanowar Elves"); 176 177 // Exclude mono-red 178 const notRed = search("c!=r"); 179 expect(notRed.ok).toBe(true); 180 if (notRed.ok) { 181 expect(notRed.value.match(bolt)).toBe(false); // R = R, excluded 182 expect(notRed.value.match(elves)).toBe(true); // G != R, included 183 } 184 }); 185 186 it("c: uses superset semantics (at least these colors)", async () => { 187 const bolt = await cards.get("Lightning Bolt"); // R 188 const bte = await cards.get("Burning-Tree Emissary"); // RG 189 190 // c:r means "at least red" - matches mono-R and multicolor with R 191 const atLeastRed = search("c:r"); 192 expect(atLeastRed.ok).toBe(true); 193 if (atLeastRed.ok) { 194 expect(atLeastRed.value.match(bolt)).toBe(true); // R contains R 195 expect(atLeastRed.value.match(bte)).toBe(true); // RG contains R 196 } 197 198 // c:rg means "at least RG" - only matches cards with both 199 const atLeastGruul = search("c:rg"); 200 expect(atLeastGruul.ok).toBe(true); 201 if (atLeastGruul.ok) { 202 expect(atLeastGruul.value.match(bolt)).toBe(false); // R doesn't contain G 203 expect(atLeastGruul.value.match(bte)).toBe(true); // RG contains RG 204 } 205 }); 206 207 it("c: differs from id: (color vs color identity)", async () => { 208 const forest = await cards.get("Forest"); 209 210 // Forest is colorless (no colored mana in cost) 211 const colorless = search("c:c"); 212 expect(colorless.ok).toBe(true); 213 if (colorless.ok) { 214 expect(colorless.value.match(forest)).toBe(true); 215 } 216 217 // But Forest has green color identity (produces green mana) 218 const greenIdentity = search("id:g"); 219 expect(greenIdentity.ok).toBe(true); 220 if (greenIdentity.ok) { 221 expect(greenIdentity.value.match(forest)).toBe(true); 222 } 223 224 // Forest is NOT green by color 225 const greenColor = search("c:g"); 226 expect(greenColor.ok).toBe(true); 227 if (greenColor.ok) { 228 expect(greenColor.value.match(forest)).toBe(false); 229 } 230 }); 231 }); 232 233 describe("color identity matching", () => { 234 it("id: uses subset semantics (commander deckbuilding)", async () => { 235 const bolt = await cards.get("Lightning Bolt"); // R 236 const elves = await cards.get("Llanowar Elves"); // G 237 const bte = await cards.get("Burning-Tree Emissary"); // RG 238 239 // id:rg means "identity fits in Gruul" (subset) 240 const gruul = search("id:rg"); 241 expect(gruul.ok).toBe(true); 242 if (gruul.ok) { 243 expect(gruul.value.match(bolt)).toBe(true); // R fits in RG 244 expect(gruul.value.match(elves)).toBe(true); // G fits in RG 245 expect(gruul.value.match(bte)).toBe(true); // RG fits in RG 246 } 247 248 // id:r should NOT match BTE (RG doesn't fit in mono-R) 249 const monoRed = search("id:r"); 250 expect(monoRed.ok).toBe(true); 251 if (monoRed.ok) { 252 expect(monoRed.value.match(bolt)).toBe(true); // R fits in R 253 expect(monoRed.value.match(bte)).toBe(false); // RG doesn't fit in R 254 } 255 256 // id>=rg means "identity contains at least RG" (superset) 257 const atLeastGruul = search("id>=rg"); 258 expect(atLeastGruul.ok).toBe(true); 259 if (atLeastGruul.ok) { 260 expect(atLeastGruul.value.match(bolt)).toBe(false); // R doesn't contain G 261 expect(atLeastGruul.value.match(elves)).toBe(false); // G doesn't contain R 262 expect(atLeastGruul.value.match(bte)).toBe(true); // RG contains RG 263 } 264 }); 265 266 it("id<= matches subset (commander deckbuilding)", async () => { 267 const bolt = await cards.get("Lightning Bolt"); 268 const elves = await cards.get("Llanowar Elves"); 269 const forest = await cards.get("Forest"); 270 271 // Gruul deck can play red and green cards 272 const gruul = search("id<=rg"); 273 expect(gruul.ok).toBe(true); 274 if (gruul.ok) { 275 expect(gruul.value.match(bolt)).toBe(true); // R fits in RG 276 expect(gruul.value.match(elves)).toBe(true); // G fits in RG 277 expect(gruul.value.match(forest)).toBe(true); // Colorless fits 278 } 279 280 // Simic deck can't play red 281 const simic = search("id<=ug"); 282 expect(simic.ok).toBe(true); 283 if (simic.ok) { 284 expect(simic.value.match(bolt)).toBe(false); // R doesn't fit 285 expect(simic.value.match(elves)).toBe(true); // G fits 286 } 287 }); 288 289 it("id!= excludes exact color identity", async () => { 290 const bolt = await cards.get("Lightning Bolt"); 291 const elves = await cards.get("Llanowar Elves"); 292 293 // Exclude mono-red identity 294 const notRed = search("id!=r"); 295 expect(notRed.ok).toBe(true); 296 if (notRed.ok) { 297 expect(notRed.value.match(bolt)).toBe(false); // R = R, excluded 298 expect(notRed.value.match(elves)).toBe(true); // G != R, included 299 } 300 301 // Exclude mono-green identity 302 const notGreen = search("id!=g"); 303 expect(notGreen.ok).toBe(true); 304 if (notGreen.ok) { 305 expect(notGreen.value.match(bolt)).toBe(true); // R != G, included 306 expect(notGreen.value.match(elves)).toBe(false); // G = G, excluded 307 } 308 }); 309 }); 310 311 describe("color identity count matching", () => { 312 const mockColorless = { color_identity: [] as string[] } as Card; 313 const mockMono = { color_identity: ["R"] } as Card; 314 const mockTwoColor = { color_identity: ["U", "R"] } as Card; 315 const mockThreeColor = { color_identity: ["W", "U", "B"] } as Card; 316 const mockFiveColor = { 317 color_identity: ["W", "U", "B", "R", "G"], 318 } as Card; 319 320 it.each([ 321 ["id=0", mockColorless, true], 322 ["id=0", mockMono, false], 323 ["id=1", mockMono, true], 324 ["id=1", mockTwoColor, false], 325 ["id=2", mockTwoColor, true], 326 ["id=3", mockThreeColor, true], 327 ["id=5", mockFiveColor, true], 328 ])( 329 "%s matches card with %d identity colors: %s", 330 (query, card, expected) => { 331 const result = search(query); 332 expect(result.ok).toBe(true); 333 if (result.ok) { 334 expect(result.value.match(card)).toBe(expected); 335 } 336 }, 337 ); 338 339 it.each([ 340 ["id>0", mockColorless, false], 341 ["id>0", mockMono, true], 342 ["id>1", mockMono, false], 343 ["id>1", mockTwoColor, true], 344 ["id>2", mockThreeColor, true], 345 ])("%s (more than N colors) matches correctly", (query, card, expected) => { 346 const result = search(query); 347 expect(result.ok).toBe(true); 348 if (result.ok) { 349 expect(result.value.match(card)).toBe(expected); 350 } 351 }); 352 353 it.each([ 354 ["id<1", mockColorless, true], 355 ["id<1", mockMono, false], 356 ["id<2", mockMono, true], 357 ["id<2", mockTwoColor, false], 358 ["id<3", mockTwoColor, true], 359 ])( 360 "%s (fewer than N colors) matches correctly", 361 (query, card, expected) => { 362 const result = search(query); 363 expect(result.ok).toBe(true); 364 if (result.ok) { 365 expect(result.value.match(card)).toBe(expected); 366 } 367 }, 368 ); 369 370 it.each([ 371 ["id>=1", mockColorless, false], 372 ["id>=1", mockMono, true], 373 ["id>=2", mockMono, false], 374 ["id>=2", mockTwoColor, true], 375 ["id<=2", mockThreeColor, false], 376 ["id<=3", mockThreeColor, true], 377 ])( 378 "%s (N or more/fewer colors) matches correctly", 379 (query, card, expected) => { 380 const result = search(query); 381 expect(result.ok).toBe(true); 382 if (result.ok) { 383 expect(result.value.match(card)).toBe(expected); 384 } 385 }, 386 ); 387 388 it.each([ 389 ["id!=1", mockMono, false], 390 ["id!=1", mockTwoColor, true], 391 ["id!=2", mockTwoColor, false], 392 ])( 393 "%s (not exactly N colors) matches correctly", 394 (query, card, expected) => { 395 const result = search(query); 396 expect(result.ok).toBe(true); 397 if (result.ok) { 398 expect(result.value.match(card)).toBe(expected); 399 } 400 }, 401 ); 402 }); 403 404 describe("color count matching", () => { 405 const mockColorless = { colors: [] as string[] } as Card; 406 const mockMono = { colors: ["R"] } as Card; 407 const mockTwoColor = { colors: ["U", "R"] } as Card; 408 const mockThreeColor = { colors: ["W", "U", "B"] } as Card; 409 const mockFiveColor = { colors: ["W", "U", "B", "R", "G"] } as Card; 410 411 it.each([ 412 ["c=0", mockColorless, true], 413 ["c=0", mockMono, false], 414 ["c=1", mockMono, true], 415 ["c=1", mockTwoColor, false], 416 ["c=2", mockTwoColor, true], 417 ["c=3", mockThreeColor, true], 418 ["c=5", mockFiveColor, true], 419 ])("%s matches card with %d colors: %s", (query, card, expected) => { 420 const result = search(query); 421 expect(result.ok).toBe(true); 422 if (result.ok) { 423 expect(result.value.match(card)).toBe(expected); 424 } 425 }); 426 427 it.each([ 428 ["c>0", mockColorless, false], 429 ["c>0", mockMono, true], 430 ["c>1", mockMono, false], 431 ["c>1", mockTwoColor, true], 432 ["c>2", mockThreeColor, true], 433 ])("%s (more than N colors) matches correctly", (query, card, expected) => { 434 const result = search(query); 435 expect(result.ok).toBe(true); 436 if (result.ok) { 437 expect(result.value.match(card)).toBe(expected); 438 } 439 }); 440 441 it.each([ 442 ["c<1", mockColorless, true], 443 ["c<1", mockMono, false], 444 ["c<2", mockMono, true], 445 ["c<2", mockTwoColor, false], 446 ["c<3", mockTwoColor, true], 447 ])( 448 "%s (fewer than N colors) matches correctly", 449 (query, card, expected) => { 450 const result = search(query); 451 expect(result.ok).toBe(true); 452 if (result.ok) { 453 expect(result.value.match(card)).toBe(expected); 454 } 455 }, 456 ); 457 458 it.each([ 459 ["c>=1", mockColorless, false], 460 ["c>=1", mockMono, true], 461 ["c>=2", mockMono, false], 462 ["c>=2", mockTwoColor, true], 463 ["c<=2", mockThreeColor, false], 464 ["c<=3", mockThreeColor, true], 465 ])( 466 "%s (N or more/fewer colors) matches correctly", 467 (query, card, expected) => { 468 const result = search(query); 469 expect(result.ok).toBe(true); 470 if (result.ok) { 471 expect(result.value.match(card)).toBe(expected); 472 } 473 }, 474 ); 475 476 it.each([ 477 ["c!=1", mockMono, false], 478 ["c!=1", mockTwoColor, true], 479 ["c!=2", mockTwoColor, false], 480 ])( 481 "%s (not exactly N colors) matches correctly", 482 (query, card, expected) => { 483 const result = search(query); 484 expect(result.ok).toBe(true); 485 if (result.ok) { 486 expect(result.value.match(card)).toBe(expected); 487 } 488 }, 489 ); 490 }); 491 492 describe("mana value matching", () => { 493 it.each([ 494 ["cmc=1", "Lightning Bolt", true], 495 ["cmc>0", "Lightning Bolt", true], 496 ["cmc>=2", "Lightning Bolt", false], 497 ["cmc<=3", "Llanowar Elves", true], 498 ["mv=1", "Sol Ring", true], 499 ])("%s matches %s: %s", async (query, cardName, expected) => { 500 const card = await cards.get(cardName); 501 const result = search(query); 502 expect(result.ok).toBe(true); 503 if (result.ok) { 504 expect(result.value.match(card)).toBe(expected); 505 } 506 }); 507 }); 508 509 describe("format legality", () => { 510 it("f: matches format legality", async () => { 511 const bolt = await cards.get("Lightning Bolt"); 512 const ring = await cards.get("Sol Ring"); 513 514 const modern = search("f:modern"); 515 expect(modern.ok).toBe(true); 516 if (modern.ok) { 517 expect(modern.value.match(bolt)).toBe(true); 518 } 519 520 const commander = search("f:commander"); 521 expect(commander.ok).toBe(true); 522 if (commander.ok) { 523 expect(commander.value.match(ring)).toBe(true); 524 } 525 }); 526 }); 527 528 describe("is: predicates", () => { 529 it("is:creature matches creatures", async () => { 530 const elves = await cards.get("Llanowar Elves"); 531 const bolt = await cards.get("Lightning Bolt"); 532 533 const result = search("is:creature"); 534 expect(result.ok).toBe(true); 535 if (result.ok) { 536 expect(result.value.match(elves)).toBe(true); 537 expect(result.value.match(bolt)).toBe(false); 538 } 539 }); 540 541 it("is:instant matches instants", async () => { 542 const bolt = await cards.get("Lightning Bolt"); 543 const result = search("is:instant"); 544 expect(result.ok).toBe(true); 545 if (result.ok) { 546 expect(result.value.match(bolt)).toBe(true); 547 } 548 }); 549 550 it("is:legendary matches legendary", async () => { 551 const elves = await cards.get("Llanowar Elves"); 552 const result = search("is:legendary"); 553 expect(result.ok).toBe(true); 554 if (result.ok) { 555 expect(result.value.match(elves)).toBe(false); 556 } 557 }); 558 }); 559 560 describe("boolean operators", () => { 561 it("implicit AND", async () => { 562 const elves = await cards.get("Llanowar Elves"); 563 const result = search("t:creature c:g"); 564 expect(result.ok).toBe(true); 565 if (result.ok) { 566 expect(result.value.match(elves)).toBe(true); 567 } 568 }); 569 570 it("explicit OR", async () => { 571 const bolt = await cards.get("Lightning Bolt"); 572 const elves = await cards.get("Llanowar Elves"); 573 574 const result = search("t:instant or t:creature"); 575 expect(result.ok).toBe(true); 576 if (result.ok) { 577 expect(result.value.match(bolt)).toBe(true); 578 expect(result.value.match(elves)).toBe(true); 579 } 580 }); 581 582 it("NOT with -", async () => { 583 const bolt = await cards.get("Lightning Bolt"); 584 const elves = await cards.get("Llanowar Elves"); 585 586 const result = search("-t:creature"); 587 expect(result.ok).toBe(true); 588 if (result.ok) { 589 expect(result.value.match(bolt)).toBe(true); 590 expect(result.value.match(elves)).toBe(false); 591 } 592 }); 593 594 it("parentheses for grouping", async () => { 595 const bolt = await cards.get("Lightning Bolt"); 596 597 const result = search("(t:instant or t:sorcery) c:r"); 598 expect(result.ok).toBe(true); 599 if (result.ok) { 600 expect(result.value.match(bolt)).toBe(true); 601 } 602 }); 603 }); 604 605 describe("rarity matching", () => { 606 // Use mock cards with explicit rarities to avoid canonical printing variance 607 const mockCommon = { rarity: "common" } as Card; 608 const mockUncommon = { rarity: "uncommon" } as Card; 609 const mockRare = { rarity: "rare" } as Card; 610 const mockMythic = { rarity: "mythic" } as Card; 611 612 it.each([ 613 ["r:c", mockCommon, true], 614 ["r:c", mockUncommon, false], 615 ["r:common", mockCommon, true], 616 ["r:u", mockUncommon, true], 617 ["r:uncommon", mockUncommon, true], 618 ["r:r", mockRare, true], 619 ["r:rare", mockRare, true], 620 ["r:m", mockMythic, true], 621 ["r:mythic", mockMythic, true], 622 ])("%s matches %s rarity: %s", (query, card, expected) => { 623 const result = search(query); 624 expect(result.ok).toBe(true); 625 if (result.ok) { 626 expect(result.value.match(card)).toBe(expected); 627 } 628 }); 629 630 it.each([ 631 ["r>=c", mockCommon, true], 632 ["r>=c", mockUncommon, true], 633 ["r>=c", mockRare, true], 634 ["r>=u", mockCommon, false], 635 ["r>=u", mockUncommon, true], 636 ["r>=u", mockRare, true], 637 ["r>=r", mockUncommon, false], 638 ["r>=r", mockRare, true], 639 ["r>=r", mockMythic, true], 640 ])("%s matches %s rarity: %s", (query, card, expected) => { 641 const result = search(query); 642 expect(result.ok).toBe(true); 643 if (result.ok) { 644 expect(result.value.match(card)).toBe(expected); 645 } 646 }); 647 648 it.each([ 649 ["r<=m", mockMythic, true], 650 ["r<=m", mockRare, true], 651 ["r<=r", mockRare, true], 652 ["r<=r", mockMythic, false], 653 ["r<=u", mockUncommon, true], 654 ["r<=u", mockRare, false], 655 ["r<=c", mockCommon, true], 656 ["r<=c", mockUncommon, false], 657 ])("%s matches %s rarity: %s", (query, card, expected) => { 658 const result = search(query); 659 expect(result.ok).toBe(true); 660 if (result.ok) { 661 expect(result.value.match(card)).toBe(expected); 662 } 663 }); 664 665 it.each([ 666 ["r>c", mockCommon, false], 667 ["r>c", mockUncommon, true], 668 ["r<u", mockCommon, true], 669 ["r<u", mockUncommon, false], 670 ])("%s matches %s rarity: %s", (query, card, expected) => { 671 const result = search(query); 672 expect(result.ok).toBe(true); 673 if (result.ok) { 674 expect(result.value.match(card)).toBe(expected); 675 } 676 }); 677 678 it("r!=c excludes common", () => { 679 const result = search("r!=c"); 680 if (!result.ok) { 681 console.log("Parse error:", result.error); 682 } 683 expect(result.ok).toBe(true); 684 if (result.ok) { 685 expect(result.value.match(mockCommon)).toBe(false); 686 expect(result.value.match(mockUncommon)).toBe(true); 687 } 688 }); 689 }); 690 691 describe("in: matching (game, set type, set, language)", () => { 692 const mockPaperCard = { 693 games: ["paper", "mtgo"], 694 set: "lea", 695 set_type: "expansion", 696 lang: "en", 697 } as Card; 698 const mockArenaCard = { 699 games: ["arena"], 700 set: "afr", 701 set_type: "expansion", 702 lang: "en", 703 } as Card; 704 const mockCommanderCard = { 705 games: ["paper"], 706 set: "cmr", 707 set_type: "commander", 708 lang: "en", 709 } as Card; 710 const mockJapaneseCard = { 711 games: ["paper"], 712 set: "sta", 713 set_type: "expansion", 714 lang: "ja", 715 } as Card; 716 717 it("in:paper matches paper games", () => { 718 const result = search("in:paper"); 719 expect(result.ok).toBe(true); 720 if (result.ok) { 721 expect(result.value.match(mockPaperCard)).toBe(true); 722 expect(result.value.match(mockArenaCard)).toBe(false); 723 } 724 }); 725 726 it("in:arena matches arena games", () => { 727 const result = search("in:arena"); 728 expect(result.ok).toBe(true); 729 if (result.ok) { 730 expect(result.value.match(mockArenaCard)).toBe(true); 731 expect(result.value.match(mockPaperCard)).toBe(false); 732 } 733 }); 734 735 it("in:mtgo matches mtgo games", () => { 736 const result = search("in:mtgo"); 737 expect(result.ok).toBe(true); 738 if (result.ok) { 739 expect(result.value.match(mockPaperCard)).toBe(true); 740 expect(result.value.match(mockArenaCard)).toBe(false); 741 } 742 }); 743 744 it("in:commander matches commander set type", () => { 745 const result = search("in:commander"); 746 expect(result.ok).toBe(true); 747 if (result.ok) { 748 expect(result.value.match(mockCommanderCard)).toBe(true); 749 expect(result.value.match(mockPaperCard)).toBe(false); 750 } 751 }); 752 753 it("in:expansion matches expansion set type", () => { 754 const result = search("in:expansion"); 755 expect(result.ok).toBe(true); 756 if (result.ok) { 757 expect(result.value.match(mockPaperCard)).toBe(true); 758 expect(result.value.match(mockCommanderCard)).toBe(false); 759 } 760 }); 761 762 it("in:<set> matches set code", () => { 763 const result = search("in:lea"); 764 expect(result.ok).toBe(true); 765 if (result.ok) { 766 expect(result.value.match(mockPaperCard)).toBe(true); 767 expect(result.value.match(mockArenaCard)).toBe(false); 768 } 769 }); 770 771 it("in:<lang> matches language", () => { 772 const result = search("in:ja"); 773 expect(result.ok).toBe(true); 774 if (result.ok) { 775 expect(result.value.match(mockJapaneseCard)).toBe(true); 776 expect(result.value.match(mockPaperCard)).toBe(false); 777 } 778 }); 779 780 it("-in:paper excludes paper cards", () => { 781 const result = search("-in:paper"); 782 expect(result.ok).toBe(true); 783 if (result.ok) { 784 expect(result.value.match(mockPaperCard)).toBe(false); 785 expect(result.value.match(mockArenaCard)).toBe(true); 786 } 787 }); 788 }); 789 790 describe("set: arena code normalization", () => { 791 const domCard = { set: "dom" } as Card; 792 const dd1Card = { set: "dd1" } as Card; 793 const evgCard = { set: "evg" } as Card; 794 795 it("set:dar finds Dominaria (dom) cards", () => { 796 // "dar" is Arena's code for Dominaria 797 const result = search("set:dar"); 798 expect(result.ok).toBe(true); 799 if (result.ok) { 800 expect(result.value.match(domCard)).toBe(true); 801 } 802 }); 803 804 it("set:dom still works directly", () => { 805 const result = search("set:dom"); 806 expect(result.ok).toBe(true); 807 if (result.ok) { 808 expect(result.value.match(domCard)).toBe(true); 809 } 810 }); 811 812 it("set:evg finds Anthology (evg), not dd1", () => { 813 // "evg" is shadowed - Arena uses it for dd1, but Scryfall has its own evg set 814 // We should NOT map it to dd1 in search to avoid hiding paper set 815 const result = search("set:evg"); 816 expect(result.ok).toBe(true); 817 if (result.ok) { 818 expect(result.value.match(evgCard)).toBe(true); 819 expect(result.value.match(dd1Card)).toBe(false); 820 } 821 }); 822 823 it("in:dar also normalizes arena codes", () => { 824 const result = search("in:dar"); 825 expect(result.ok).toBe(true); 826 if (result.ok) { 827 expect(result.value.match(domCard)).toBe(true); 828 } 829 }); 830 }); 831 832 describe("complex queries", () => { 833 it("commander deckbuilding query", async () => { 834 const elves = await cards.get("Llanowar Elves"); 835 836 // Find green creatures with cmc <= 2 for a Golgari commander deck 837 const result = search("t:creature id<=bg cmc<=2"); 838 expect(result.ok).toBe(true); 839 if (result.ok) { 840 expect(result.value.match(elves)).toBe(true); 841 } 842 }); 843 844 it("negated color with type", async () => { 845 const bolt = await cards.get("Lightning Bolt"); 846 const elves = await cards.get("Llanowar Elves"); 847 848 // Red non-creatures 849 const result = search("c:r -t:creature"); 850 expect(result.ok).toBe(true); 851 if (result.ok) { 852 expect(result.value.match(bolt)).toBe(true); 853 expect(result.value.match(elves)).toBe(false); 854 } 855 }); 856 }); 857});