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