👁️
6
fork

Configure Feed

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

at dev 347 lines 9.6 kB view raw
1import { beforeAll, describe, expect, it } from "vitest"; 2import { 3 setupTestCards, 4 type TestCardLookup, 5} from "../../__tests__/test-card-lookup"; 6import { search } from "../index"; 7 8describe("Scryfall search edge cases", () => { 9 let cards: TestCardLookup; 10 11 beforeAll(async () => { 12 cards = await setupTestCards(); 13 }, 30_000); 14 15 describe("power/toughness edge cases", () => { 16 it("matches * power exactly", async () => { 17 const tarmogoyf = await cards.get("Tarmogoyf"); 18 expect(tarmogoyf.power).toBe("*"); 19 20 const result = search("pow=*"); 21 expect(result.ok).toBe(true); 22 if (result.ok) { 23 expect(result.value.match(tarmogoyf)).toBe(true); 24 } 25 }); 26 27 it("matches 1+* toughness (contains *)", async () => { 28 const tarmogoyf = await cards.get("Tarmogoyf"); 29 expect(tarmogoyf.toughness).toBe("1+*"); 30 31 const result = search("tou=*"); 32 expect(result.ok).toBe(true); 33 if (result.ok) { 34 // Should match because toughness contains * 35 expect(result.value.match(tarmogoyf)).toBe(true); 36 } 37 }); 38 39 it("treats * as 0 for numeric comparisons", async () => { 40 const tarmogoyf = await cards.get("Tarmogoyf"); 41 42 const result = search("pow<=0"); 43 expect(result.ok).toBe(true); 44 if (result.ok) { 45 // * is treated as 0 46 expect(result.value.match(tarmogoyf)).toBe(true); 47 } 48 49 const gtZero = search("pow>0"); 50 expect(gtZero.ok).toBe(true); 51 if (gtZero.ok) { 52 expect(gtZero.value.match(tarmogoyf)).toBe(false); 53 } 54 }); 55 56 it("handles fractional power/toughness", async () => { 57 const littleGirl = await cards.get("Little Girl"); 58 // Scryfall stores ".5" not "0.5" 59 expect(littleGirl.power).toBe(".5"); 60 expect(littleGirl.toughness).toBe(".5"); 61 62 // parseFloat handles ".5" correctly as 0.5 63 const exactHalf = search("pow=0.5"); 64 expect(exactHalf.ok).toBe(true); 65 if (exactHalf.ok) { 66 expect(exactHalf.value.match(littleGirl)).toBe(true); 67 } 68 69 // Comparison with decimal 70 const ltOne = search("pow<1"); 71 expect(ltOne.ok).toBe(true); 72 if (ltOne.ok) { 73 expect(ltOne.value.match(littleGirl)).toBe(true); 74 } 75 76 const gtZero = search("pow>0"); 77 expect(gtZero.ok).toBe(true); 78 if (gtZero.ok) { 79 expect(gtZero.value.match(littleGirl)).toBe(true); 80 } 81 }); 82 }); 83 84 describe("mana value edge cases", () => { 85 it("handles fractional CMC", async () => { 86 const littleGirl = await cards.get("Little Girl"); 87 expect(littleGirl.cmc).toBe(0.5); 88 89 const exactHalf = search("cmc=0.5"); 90 expect(exactHalf.ok).toBe(true); 91 if (exactHalf.ok) { 92 expect(exactHalf.value.match(littleGirl)).toBe(true); 93 } 94 95 const ltOne = search("cmc<1"); 96 expect(ltOne.ok).toBe(true); 97 if (ltOne.ok) { 98 expect(ltOne.value.match(littleGirl)).toBe(true); 99 } 100 }); 101 102 it("handles X spells (X doesn't add to CMC)", async () => { 103 const fireball = await cards.get("Fireball"); 104 expect(fireball.mana_cost).toContain("X"); 105 106 // Fireball is {X}{R}, CMC = 1 107 const cmcOne = search("cmc=1"); 108 expect(cmcOne.ok).toBe(true); 109 if (cmcOne.ok) { 110 expect(cmcOne.value.match(fireball)).toBe(true); 111 } 112 }); 113 }); 114 115 describe("mana cost matching", () => { 116 it("m: matches mana cost substring", async () => { 117 const apostle = await cards.get("Apostle's Blessing"); 118 expect(apostle.mana_cost).toBe("{1}{W/P}"); 119 120 // Phyrexian mana symbol matching - braces included like Scryfall 121 const result = search("m:{W/P}"); 122 expect(result.ok).toBe(true); 123 if (result.ok) { 124 expect(result.value.match(apostle)).toBe(true); 125 } 126 }); 127 128 it("m: matches snow mana", async () => { 129 const astrolabe = await cards.get("Arcum's Astrolabe"); 130 expect(astrolabe.mana_cost).toBe("{S}"); 131 132 const result = search("m:S"); 133 expect(result.ok).toBe(true); 134 if (result.ok) { 135 expect(result.value.match(astrolabe)).toBe(true); 136 } 137 }); 138 139 it("m: matches X in cost", async () => { 140 const fireball = await cards.get("Fireball"); 141 142 const result = search("m:X"); 143 expect(result.ok).toBe(true); 144 if (result.ok) { 145 expect(result.value.match(fireball)).toBe(true); 146 } 147 }); 148 }); 149 150 describe("is:snow predicate", () => { 151 it("matches snow permanents", async () => { 152 const snowForest = await cards.get("Snow-Covered Forest"); 153 expect(snowForest.type_line).toContain("Snow"); 154 155 const result = search("is:snow"); 156 expect(result.ok).toBe(true); 157 if (result.ok) { 158 expect(result.value.match(snowForest)).toBe(true); 159 } 160 }); 161 162 it("does not match non-snow cards", async () => { 163 const bolt = await cards.get("Lightning Bolt"); 164 165 const result = search("is:snow"); 166 expect(result.ok).toBe(true); 167 if (result.ok) { 168 expect(result.value.match(bolt)).toBe(false); 169 } 170 }); 171 }); 172 173 describe("colorless identity", () => { 174 it("id:c matches colorless identity cards", async () => { 175 const ornithopter = await cards.get("Ornithopter"); 176 expect(ornithopter.color_identity).toEqual([]); 177 178 const result = search("id:c"); 179 expect(result.ok).toBe(true); 180 if (result.ok) { 181 expect(result.value.match(ornithopter)).toBe(true); 182 } 183 }); 184 185 it("id<=c matches only colorless cards", async () => { 186 const ornithopter = await cards.get("Ornithopter"); 187 const bolt = await cards.get("Lightning Bolt"); 188 189 const result = search("id<=c"); 190 expect(result.ok).toBe(true); 191 if (result.ok) { 192 expect(result.value.match(ornithopter)).toBe(true); 193 expect(result.value.match(bolt)).toBe(false); 194 } 195 }); 196 }); 197 198 describe("is:historic predicate", () => { 199 it("matches legendary permanents", async () => { 200 const bosh = await cards.get("Bosh, Iron Golem"); 201 expect(bosh.type_line).toContain("Legendary"); 202 203 const result = search("is:historic"); 204 expect(result.ok).toBe(true); 205 if (result.ok) { 206 expect(result.value.match(bosh)).toBe(true); 207 } 208 }); 209 210 it("matches artifacts", async () => { 211 const solRing = await cards.get("Sol Ring"); 212 expect(solRing.type_line).toContain("Artifact"); 213 214 const result = search("is:historic"); 215 expect(result.ok).toBe(true); 216 if (result.ok) { 217 expect(result.value.match(solRing)).toBe(true); 218 } 219 }); 220 221 it("does not match non-historic cards", async () => { 222 const bolt = await cards.get("Lightning Bolt"); 223 224 const result = search("is:historic"); 225 expect(result.ok).toBe(true); 226 if (result.ok) { 227 expect(result.value.match(bolt)).toBe(false); 228 } 229 }); 230 }); 231 232 describe("complex mana and color queries", () => { 233 it("Phyrexian mana cards are in the color's identity", async () => { 234 const apostle = await cards.get("Apostle's Blessing"); 235 // W/P adds W to color identity even though it can be paid with life 236 expect(apostle.color_identity).toContain("W"); 237 238 const whiteId = search("id:w"); 239 expect(whiteId.ok).toBe(true); 240 if (whiteId.ok) { 241 expect(whiteId.value.match(apostle)).toBe(true); 242 } 243 }); 244 }); 245 246 describe("loyalty matching", () => { 247 it("matches planeswalker loyalty", async () => { 248 const bolas = await cards.get("Nicol Bolas, Planeswalker"); 249 expect(bolas.loyalty).toBeDefined(); 250 251 const result = search("loy>=5"); 252 expect(result.ok).toBe(true); 253 if (result.ok) { 254 expect(result.value.match(bolas)).toBe(true); 255 } 256 }); 257 258 it("non-planeswalkers have no loyalty", async () => { 259 const bolt = await cards.get("Lightning Bolt"); 260 261 const result = search("loy>0"); 262 expect(result.ok).toBe(true); 263 if (result.ok) { 264 expect(result.value.match(bolt)).toBe(false); 265 } 266 }); 267 }); 268 269 describe("multi-face cards", () => { 270 it("searches oracle text across faces", async () => { 271 const delver = await cards.get("Delver of Secrets"); 272 // Delver transforms into Insectile Aberration 273 274 // Should match front face 275 const front = search('o:"Look at the top card"'); 276 expect(front.ok).toBe(true); 277 if (front.ok) { 278 expect(front.value.match(delver)).toBe(true); 279 } 280 }); 281 282 it("is:transform matches transform cards", async () => { 283 const delver = await cards.get("Delver of Secrets"); 284 const bolt = await cards.get("Lightning Bolt"); 285 286 const result = search("is:transform"); 287 expect(result.ok).toBe(true); 288 if (result.ok) { 289 expect(result.value.match(delver)).toBe(true); 290 expect(result.value.match(bolt)).toBe(false); 291 } 292 }); 293 }); 294 295 describe("regex edge cases", () => { 296 it("regex at start of query works", async () => { 297 const bolt = await cards.get("Lightning Bolt"); 298 299 const result = search("/^lightning/i"); 300 expect(result.ok).toBe(true); 301 if (result.ok) { 302 expect(result.value.match(bolt)).toBe(true); 303 } 304 }); 305 306 it("regex after field works", async () => { 307 const bolt = await cards.get("Lightning Bolt"); 308 309 const result = search("o:/\\d+ damage/"); 310 expect(result.ok).toBe(true); 311 if (result.ok) { 312 expect(result.value.match(bolt)).toBe(true); 313 } 314 }); 315 316 it("invalid regex returns error", () => { 317 const result = search("/[invalid/"); 318 expect(result.ok).toBe(false); 319 if (!result.ok) { 320 expect(result.error.message).toContain("Invalid regex"); 321 } 322 }); 323 }); 324 325 describe("produces: mana production", () => { 326 it("matches cards that produce mana", async () => { 327 const solRing = await cards.get("Sol Ring"); 328 expect(solRing.produced_mana).toContain("C"); 329 330 const result = search("produces:c"); 331 expect(result.ok).toBe(true); 332 if (result.ok) { 333 expect(result.value.match(solRing)).toBe(true); 334 } 335 }); 336 337 it("non-mana-producers don't match", async () => { 338 const bolt = await cards.get("Lightning Bolt"); 339 340 const result = search("produces:r"); 341 expect(result.ok).toBe(true); 342 if (result.ok) { 343 expect(result.value.match(bolt)).toBe(false); 344 } 345 }); 346 }); 347});