@axiapps/gw2-data 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/data/overrides.json +24 -0
  2. package/package.json +3 -1
  3. package/src/engine/attributes.js +102 -23
  4. package/src/engine/boons.js +19 -1
  5. package/src/engine/constants.js +27 -2
  6. package/src/engine/index.js +4 -4
  7. package/src/engine/modifiers.js +57 -21
  8. package/src/wiki/parser.js +17 -9
  9. package/scripts/generate-fixtures.js +0 -242
  10. package/tests/api-client.test.js +0 -138
  11. package/tests/cache.test.js +0 -108
  12. package/tests/engine/attributes.test.js +0 -252
  13. package/tests/engine/boons.test.js +0 -129
  14. package/tests/engine/combos.test.js +0 -76
  15. package/tests/engine/constants.test.js +0 -576
  16. package/tests/engine/fixtures/berserker-thief.json +0 -61
  17. package/tests/engine/fixtures/berserker-warrior.json +0 -113
  18. package/tests/engine/fixtures/celestial-firebrand-wvw.json +0 -94
  19. package/tests/engine/fixtures/harrier-druid.json +0 -119
  20. package/tests/engine/fixtures/viper-mirage.json +0 -104
  21. package/tests/engine/graph.test.js +0 -30
  22. package/tests/engine/integration.test.js +0 -111
  23. package/tests/engine/modifiers.test.js +0 -473
  24. package/tests/engine/overrides.test.js +0 -70
  25. package/tests/engine/snapshot.test.js +0 -53
  26. package/tests/engine/test-utils.js +0 -20
  27. package/tests/engine/tooltips.test.js +0 -62
  28. package/tests/fixtures/capture.js +0 -160
  29. package/tests/fixtures/fixtures.json +0 -839
  30. package/tests/integration.test.js +0 -100
  31. package/tests/match.test.js +0 -176
  32. package/tests/merge.test.js +0 -128
  33. package/tests/normalize.test.js +0 -78
  34. package/tests/parser.test.js +0 -506
  35. package/tests/real-data.test.js +0 -296
  36. package/tests/relations.test.js +0 -80
  37. package/tests/resolver.test.js +0 -721
  38. package/tests/validate-live.js +0 -191
  39. package/tests/wiki-client.test.js +0 -468
  40. package/tests/wiki-integration.test.js +0 -177
  41. package/tests/wiki-live-validation.test.js +0 -61
  42. package/tests/wiki-snapshots.test.js +0 -166
@@ -1,721 +0,0 @@
1
- "use strict";
2
-
3
- const {
4
- groupFactsByMode,
5
- parseFactsByMode,
6
- resolveEntityFacts,
7
- isDisambiguation,
8
- extractInfoboxId,
9
- } = require("../src/wiki/resolver");
10
- const { WikiClient } = require("../src/wiki/client");
11
- const { MemoryCache } = require("../src/wiki/cache");
12
-
13
- describe("groupFactsByMode", () => {
14
- test("universal facts go to all three arrays", () => {
15
- const facts = [
16
- { type: "Damage", text: "Damage", dmg_multiplier: 0.8, hit_count: 1, _modes: ["pve", "wvw", "pvp"] },
17
- ];
18
- const result = groupFactsByMode(facts);
19
-
20
- expect(result.pve).toHaveLength(1);
21
- expect(result.wvw).toHaveLength(1);
22
- expect(result.pvp).toHaveLength(1);
23
- expect(result.pve[0]).toEqual({ type: "Damage", text: "Damage", dmg_multiplier: 0.8, hit_count: 1 });
24
- expect(result.wvw[0]).toEqual({ type: "Damage", text: "Damage", dmg_multiplier: 0.8, hit_count: 1 });
25
- expect(result.pvp[0]).toEqual({ type: "Damage", text: "Damage", dmg_multiplier: 0.8, hit_count: 1 });
26
- });
27
-
28
- test("pve-only fact goes only to pve", () => {
29
- const facts = [
30
- { type: "Recharge", text: "Recharge", value: 10, _modes: ["pve"] },
31
- ];
32
- const result = groupFactsByMode(facts);
33
-
34
- expect(result.pve).toHaveLength(1);
35
- expect(result.wvw).toHaveLength(0);
36
- expect(result.pvp).toHaveLength(0);
37
- });
38
-
39
- test("wvw+pvp fact goes to both but not pve", () => {
40
- const facts = [
41
- { type: "Recharge", text: "Recharge", value: 15, _modes: ["wvw", "pvp"] },
42
- ];
43
- const result = groupFactsByMode(facts);
44
-
45
- expect(result.pve).toHaveLength(0);
46
- expect(result.wvw).toHaveLength(1);
47
- expect(result.pvp).toHaveLength(1);
48
- });
49
-
50
- test("mixed universal and mode-specific facts", () => {
51
- const facts = [
52
- { type: "Damage", text: "Damage", dmg_multiplier: 0.8, hit_count: 1, _modes: ["pve", "wvw", "pvp"] },
53
- { type: "Recharge", text: "Recharge", value: 10, _modes: ["pve"] },
54
- { type: "Recharge", text: "Recharge", value: 15, _modes: ["wvw", "pvp"] },
55
- ];
56
- const result = groupFactsByMode(facts);
57
-
58
- expect(result.pve).toHaveLength(2); // damage + pve recharge
59
- expect(result.wvw).toHaveLength(2); // damage + wvw/pvp recharge
60
- expect(result.pvp).toHaveLength(2); // damage + wvw/pvp recharge
61
- });
62
-
63
- test("_modes is stripped from output facts", () => {
64
- const facts = [
65
- { type: "Damage", text: "Damage", dmg_multiplier: 1.0, hit_count: 1, _modes: ["pve", "wvw", "pvp"] },
66
- ];
67
- const result = groupFactsByMode(facts);
68
-
69
- expect(result.pve[0]._modes).toBeUndefined();
70
- expect(result.wvw[0]._modes).toBeUndefined();
71
- expect(result.pvp[0]._modes).toBeUndefined();
72
- });
73
- });
74
-
75
- describe("parseFactsByMode", () => {
76
- test("simple skill with no split returns pve facts, null-able wvw/pvp", () => {
77
- const wikitext = "{{skill fact|damage|0.8}}\n{{skill fact|recharge|10}}";
78
- const result = parseFactsByMode(wikitext);
79
-
80
- expect(result.pve.length).toBeGreaterThan(0);
81
- expect(result.hasSplit).toBe(false);
82
- // wvw/pvp get facts too (universal), but hasSplit is false
83
- // so the caller (resolveEntityFacts) would set them to null
84
- });
85
-
86
- test("skill with pve/wvw split separates correctly", () => {
87
- const wikitext = [
88
- "| split = pve, wvw, pvp",
89
- "{{skill fact|damage|0.8}}",
90
- "{{skill fact|recharge|10|game mode = pve}}",
91
- "{{skill fact|recharge|15|game mode = wvw pvp}}",
92
- ].join("\n");
93
- const result = parseFactsByMode(wikitext);
94
-
95
- expect(result.hasSplit).toBe(true);
96
- // PvE should have damage + pve recharge
97
- const pveRecharge = result.pve.find((f) => f.type === "Recharge");
98
- expect(pveRecharge.value).toBe(10);
99
- // WvW should have damage + wvw/pvp recharge
100
- const wvwRecharge = result.wvw.find((f) => f.type === "Recharge");
101
- expect(wvwRecharge.value).toBe(15);
102
- });
103
-
104
- test("skill with only universal facts but split marker still returns hasSplit", () => {
105
- const wikitext = [
106
- "| split = pve, wvw pvp",
107
- "{{skill fact|damage|0.8}}",
108
- "| recharge pvp = 25",
109
- ].join("\n");
110
- const result = parseFactsByMode(wikitext);
111
-
112
- // splitGrouping.wvwHasSplit is true (wvw grouped with pvp => wvwHasSplit true)
113
- expect(result.hasSplit).toBe(true);
114
- // wvw/pvp should have the universal damage fact
115
- expect(result.wvw.length).toBeGreaterThan(0);
116
- expect(result.pvp.length).toBeGreaterThan(0);
117
- });
118
- });
119
-
120
- describe("resolveEntityFacts", () => {
121
- let client;
122
- let mockFetch;
123
-
124
- beforeEach(() => {
125
- mockFetch = jest.fn();
126
- client = new WikiClient({ cache: new MemoryCache(), fetch: mockFetch });
127
- });
128
-
129
- test("resolves facts for multiple entities", async () => {
130
- mockFetch.mockResolvedValueOnce({
131
- ok: true,
132
- json: async () => ({
133
- query: {
134
- pages: {
135
- "1": {
136
- title: "Fireball",
137
- revisions: [{ "*": "{{skill fact|damage|0.8}}" }],
138
- },
139
- "2": {
140
- title: "Heal",
141
- revisions: [{ "*": "{{skill fact|healing|372|coefficient=0.25}}" }],
142
- },
143
- },
144
- },
145
- }),
146
- });
147
-
148
- const idToTitle = new Map([
149
- [5489, "Fireball"],
150
- [5503, "Heal"],
151
- ]);
152
-
153
- const result = await resolveEntityFacts(client, idToTitle);
154
-
155
- expect(result.size).toBe(2);
156
- expect(result.get(5489).pve.length).toBeGreaterThan(0);
157
- expect(result.get(5489).pve[0].type).toBe("Damage");
158
- expect(result.get(5503).pve.length).toBeGreaterThan(0);
159
- expect(result.get(5503).pve[0].type).toBe("AttributeAdjust");
160
- // No split, so wvw/pvp should be null
161
- expect(result.get(5489).wvw).toBeNull();
162
- expect(result.get(5489).pvp).toBeNull();
163
- });
164
-
165
- test("skips missing wiki pages", async () => {
166
- mockFetch.mockResolvedValueOnce({
167
- ok: true,
168
- json: async () => ({
169
- query: {
170
- pages: {
171
- "1": {
172
- title: "Fireball",
173
- revisions: [{ "*": "{{skill fact|damage|0.8}}" }],
174
- },
175
- "-1": {
176
- title: "Nonexistent Skill",
177
- missing: true,
178
- },
179
- },
180
- },
181
- }),
182
- });
183
-
184
- const idToTitle = new Map([
185
- [5489, "Fireball"],
186
- [9999, "Nonexistent Skill"],
187
- ]);
188
-
189
- const result = await resolveEntityFacts(client, idToTitle);
190
-
191
- expect(result.size).toBe(1);
192
- expect(result.has(5489)).toBe(true);
193
- expect(result.has(9999)).toBe(false);
194
- });
195
-
196
- test("returns empty map for empty input", async () => {
197
- const idToTitle = new Map();
198
- const result = await resolveEntityFacts(client, idToTitle);
199
-
200
- expect(result.size).toBe(0);
201
- expect(mockFetch).not.toHaveBeenCalled();
202
- });
203
-
204
- test("retries disambiguation pages with profession-specific suffix", async () => {
205
- const disambigWikitext = "'''Charge''' may refer to:\n{{disambig}}\n* [[Charge (warrior skill)]]\n* [[Charge (ranger skill)]]";
206
- const realWikitext = "{{skill fact|damage|0.8}}\n| recharge = 10";
207
-
208
- // First fetch: returns the disambig page
209
- mockFetch.mockResolvedValueOnce({
210
- ok: true,
211
- json: async () => ({
212
- query: {
213
- pages: {
214
- "100": { title: "Charge", revisions: [{ "*": disambigWikitext }] },
215
- },
216
- },
217
- }),
218
- });
219
- // Second fetch: retry with "Charge (warrior skill)"
220
- mockFetch.mockResolvedValueOnce({
221
- ok: true,
222
- json: async () => ({
223
- query: {
224
- pages: {
225
- "200": { title: "Charge (warrior skill)", revisions: [{ "*": realWikitext }] },
226
- },
227
- },
228
- }),
229
- });
230
-
231
- const idToTitle = new Map([[14401, "Charge"]]);
232
- const result = await resolveEntityFacts(client, idToTitle, { profession: "Warrior" });
233
-
234
- expect(result.size).toBe(1);
235
- expect(result.has(14401)).toBe(true);
236
- expect(result.get(14401).pve[0].type).toBe("Damage");
237
- expect(result.get(14401).recharge.pve).toBe(10);
238
- // Should have made 2 fetch calls: original batch + retry batch
239
- expect(mockFetch).toHaveBeenCalledTimes(2);
240
- });
241
-
242
- test("skips disambiguation pages when no profession provided", async () => {
243
- const disambigWikitext = "'''Charge''' may refer to:\n{{disambig}}";
244
- mockFetch.mockResolvedValueOnce({
245
- ok: true,
246
- json: async () => ({
247
- query: {
248
- pages: {
249
- "100": { title: "Charge", revisions: [{ "*": disambigWikitext }] },
250
- },
251
- },
252
- }),
253
- });
254
-
255
- const idToTitle = new Map([[14401, "Charge"]]);
256
- const result = await resolveEntityFacts(client, idToTitle);
257
-
258
- expect(result.size).toBe(0);
259
- expect(mockFetch).toHaveBeenCalledTimes(1);
260
- });
261
-
262
- test("retries with prefix search when page has wrong infobox type", async () => {
263
- const locationWikitext = "{{Location infobox\n| name = Ring of Fire\n| id = 20\n}}";
264
- const skillWikitext = "{{Skill infobox\n| id = 5765\n}}\n{{skill fact|damage|1.0}}";
265
-
266
- // Initial batch fetch returns location page
267
- mockFetch.mockResolvedValueOnce({
268
- ok: true,
269
- json: async () => ({
270
- query: {
271
- pages: {
272
- "1": { title: "Ring of Fire", revisions: [{ "*": locationWikitext }] },
273
- },
274
- },
275
- }),
276
- });
277
-
278
- // Prefix search for "Ring of Fire (" returns candidates
279
- mockFetch.mockResolvedValueOnce({
280
- ok: true,
281
- json: async () => ({
282
- query: {
283
- prefixsearch: [
284
- { title: "Ring of Fire (elementalist skill)" },
285
- { title: "Ring of Fire (location)" },
286
- ],
287
- },
288
- }),
289
- });
290
-
291
- // Batch fetch of skill candidate (location filtered out by regex)
292
- mockFetch.mockResolvedValueOnce({
293
- ok: true,
294
- json: async () => ({
295
- query: {
296
- pages: {
297
- "2": { title: "Ring of Fire (elementalist skill)", revisions: [{ "*": skillWikitext }] },
298
- },
299
- },
300
- }),
301
- });
302
-
303
- const idToTitle = new Map([[5765, "Ring of Fire"]]);
304
- const result = await resolveEntityFacts(client, idToTitle, { profession: "Elementalist" });
305
-
306
- expect(result.size).toBe(1);
307
- expect(result.has(5765)).toBe(true);
308
- expect(result.get(5765).pve[0].type).toBe("Damage");
309
- });
310
-
311
- test("prefix search finds generic (skill) suffix", async () => {
312
- const weaponWikitext = "{{Weapon infobox\n| type = Sword\n| id = 29181\n}}";
313
- const skillWikitext = "{{Skill infobox\n| id = 63281\n}}\n{{skill fact|damage|0.5}}";
314
-
315
- mockFetch.mockResolvedValueOnce({
316
- ok: true,
317
- json: async () => ({
318
- query: {
319
- pages: {
320
- "1": { title: "Zap", revisions: [{ "*": weaponWikitext }] },
321
- },
322
- },
323
- }),
324
- });
325
-
326
- // Prefix search returns only the generic (skill) page
327
- mockFetch.mockResolvedValueOnce({
328
- ok: true,
329
- json: async () => ({
330
- query: {
331
- prefixsearch: [{ title: "Zap (skill)" }],
332
- },
333
- }),
334
- });
335
-
336
- mockFetch.mockResolvedValueOnce({
337
- ok: true,
338
- json: async () => ({
339
- query: {
340
- pages: {
341
- "3": { title: "Zap (skill)", revisions: [{ "*": skillWikitext }] },
342
- },
343
- },
344
- }),
345
- });
346
-
347
- const idToTitle = new Map([[63281, "Zap"]]);
348
- const result = await resolveEntityFacts(client, idToTitle, { profession: "Elementalist" });
349
-
350
- expect(result.size).toBe(1);
351
- expect(result.has(63281)).toBe(true);
352
- expect(result.get(63281).pve[0].type).toBe("Damage");
353
- });
354
-
355
- test("accepts page directly when it has correct infobox type", async () => {
356
- const skillWikitext = "{{Skill infobox\n| id = 5489\n}}\n{{skill fact|damage|0.8}}";
357
-
358
- mockFetch.mockResolvedValueOnce({
359
- ok: true,
360
- json: async () => ({
361
- query: {
362
- pages: {
363
- "1": { title: "Fireball", revisions: [{ "*": skillWikitext }] },
364
- },
365
- },
366
- }),
367
- });
368
-
369
- const idToTitle = new Map([[5489, "Fireball"]]);
370
- const result = await resolveEntityFacts(client, idToTitle, { profession: "Elementalist" });
371
-
372
- expect(result.size).toBe(1);
373
- expect(result.has(5489)).toBe(true);
374
- expect(mockFetch).toHaveBeenCalledTimes(1);
375
- });
376
-
377
- test("falls back to API facts when prefix search finds no skill/trait candidates", async () => {
378
- const locationWikitext = "{{Location infobox\n| name = Some Place\n| id = 99\n}}";
379
-
380
- mockFetch.mockResolvedValueOnce({
381
- ok: true,
382
- json: async () => ({
383
- query: {
384
- pages: {
385
- "1": { title: "Some Place", revisions: [{ "*": locationWikitext }] },
386
- },
387
- },
388
- }),
389
- });
390
-
391
- // Prefix search returns no skill/trait candidates
392
- mockFetch.mockResolvedValueOnce({
393
- ok: true,
394
- json: async () => ({
395
- query: {
396
- prefixsearch: [{ title: "Some Place (location)" }],
397
- },
398
- }),
399
- });
400
-
401
- // Batch fetch of the candidate page (location infobox, not skill/trait)
402
- mockFetch.mockResolvedValueOnce({
403
- ok: true,
404
- json: async () => ({
405
- query: {
406
- pages: {
407
- "2": { title: "Some Place (location)", revisions: [{ "*": "{{Location infobox\n| name = Some Place\n| id = 99\n}}" }] },
408
- },
409
- },
410
- }),
411
- });
412
-
413
- const idToTitle = new Map([[12345, "Some Place"]]);
414
- const result = await resolveEntityFacts(client, idToTitle, { profession: "Warrior" });
415
-
416
- expect(result.size).toBe(0);
417
- });
418
-
419
- test("prefix search finds (trait skill) suffix", async () => {
420
- const locationWikitext = "{{Location infobox\n| name = Pulmonary Impact\n| id = 99\n}}";
421
- const traitSkillWikitext = "{{Skill infobox\n| id = 62710\n}}\n{{skill fact|damage|1.2}}";
422
-
423
- mockFetch.mockResolvedValueOnce({
424
- ok: true,
425
- json: async () => ({
426
- query: {
427
- pages: {
428
- "1": { title: "Pulmonary Impact", revisions: [{ "*": locationWikitext }] },
429
- },
430
- },
431
- }),
432
- });
433
-
434
- // Prefix search finds the (trait skill) page
435
- mockFetch.mockResolvedValueOnce({
436
- ok: true,
437
- json: async () => ({
438
- query: {
439
- prefixsearch: [{ title: "Pulmonary Impact (trait skill)" }],
440
- },
441
- }),
442
- });
443
-
444
- mockFetch.mockResolvedValueOnce({
445
- ok: true,
446
- json: async () => ({
447
- query: {
448
- pages: {
449
- "5": { title: "Pulmonary Impact (trait skill)", revisions: [{ "*": traitSkillWikitext }] },
450
- },
451
- },
452
- }),
453
- });
454
-
455
- const idToTitle = new Map([[62710, "Pulmonary Impact"]]);
456
- const result = await resolveEntityFacts(client, idToTitle, { profession: "Harbinger" });
457
-
458
- expect(result.size).toBe(1);
459
- expect(result.has(62710)).toBe(true);
460
- expect(result.get(62710).pve[0].type).toBe("Damage");
461
- });
462
-
463
- test("prefix search works without profession option", async () => {
464
- const locationWikitext = "{{Location infobox\n| name = Some Skill\n| id = 99\n}}";
465
- const skillWikitext = "{{Skill infobox\n| id = 1234\n}}\n{{skill fact|damage|0.5}}";
466
-
467
- mockFetch.mockResolvedValueOnce({
468
- ok: true,
469
- json: async () => ({
470
- query: {
471
- pages: {
472
- "1": { title: "Some Skill", revisions: [{ "*": locationWikitext }] },
473
- },
474
- },
475
- }),
476
- });
477
-
478
- mockFetch.mockResolvedValueOnce({
479
- ok: true,
480
- json: async () => ({
481
- query: {
482
- prefixsearch: [{ title: "Some Skill (skill)" }],
483
- },
484
- }),
485
- });
486
-
487
- mockFetch.mockResolvedValueOnce({
488
- ok: true,
489
- json: async () => ({
490
- query: {
491
- pages: {
492
- "2": { title: "Some Skill (skill)", revisions: [{ "*": skillWikitext }] },
493
- },
494
- },
495
- }),
496
- });
497
-
498
- const idToTitle = new Map([[1234, "Some Skill"]]);
499
- const result = await resolveEntityFacts(client, idToTitle); // no profession
500
-
501
- expect(result.size).toBe(1);
502
- expect(result.has(1234)).toBe(true);
503
- });
504
-
505
- test("resolves different facts for entities with disambiguated titles", async () => {
506
- const weaponSkillWikitext = "{{Skill infobox\n| id = 12525\n}}\n{{skill fact|damage|1.5}}\n| recharge = 4";
507
- const petSkillWikitext = "{{Skill infobox\n| id = 41406\n}}\n{{skill fact|damage|0.8}}\n| recharge = 10";
508
-
509
- // Both titles fetched in a single batch
510
- mockFetch.mockResolvedValueOnce({
511
- ok: true,
512
- json: async () => ({
513
- query: {
514
- pages: {
515
- "1": { title: "Maul (ranger greatsword skill)", revisions: [{ "*": weaponSkillWikitext }] },
516
- "2": { title: "Maul (porcine)", revisions: [{ "*": petSkillWikitext }] },
517
- },
518
- },
519
- }),
520
- });
521
-
522
- // Two different IDs, each with their own disambiguated title
523
- const idToTitle = new Map([
524
- [12525, "Maul (ranger greatsword skill)"],
525
- [41406, "Maul (porcine)"],
526
- ]);
527
-
528
- const result = await resolveEntityFacts(client, idToTitle, { profession: "Ranger" });
529
-
530
- expect(result.size).toBe(2);
531
- // Weapon skill gets its own facts
532
- expect(result.get(12525).pve[0].type).toBe("Damage");
533
- expect(result.get(12525).recharge.pve).toBe(4);
534
- // Pet skill gets different facts
535
- expect(result.get(41406).pve[0].type).toBe("Damage");
536
- expect(result.get(41406).recharge.pve).toBe(10);
537
- // Only one batch fetch needed (both titles in the same request)
538
- expect(mockFetch).toHaveBeenCalledTimes(1);
539
- });
540
-
541
- test("uses infobox ID matching to refine facts for colliding names", async () => {
542
- // Wiki page "Maul" is about the ranger greatsword skill (id 12525).
543
- // A pet skill (id 41406) shares the name "Maul" — it initially gets the same
544
- // facts, but the resolver's prefix search finds a better-matched page and
545
- // overwrites with the correct facts.
546
- const mainPageWikitext = "{{Skill infobox\n| id = 12525\n}}\n{{skill fact|damage|1.5}}\n| recharge = 4";
547
- const petPageWikitext = "{{Skill infobox\n| id = 41406\n}}\n{{skill fact|damage|0.8}}\n| recharge = 10";
548
-
549
- // Initial batch: both IDs request "Maul"
550
- mockFetch.mockResolvedValueOnce({
551
- ok: true,
552
- json: async () => ({
553
- query: {
554
- pages: {
555
- "1": { title: "Maul", revisions: [{ "*": mainPageWikitext }] },
556
- },
557
- },
558
- }),
559
- });
560
-
561
- // Prefix search for "Maul (" returns candidates
562
- mockFetch.mockResolvedValueOnce({
563
- ok: true,
564
- json: async () => ({
565
- query: {
566
- prefixsearch: [
567
- { title: "Maul (porcine)" },
568
- { title: "Maul (ranger greatsword skill)" },
569
- ],
570
- },
571
- }),
572
- });
573
-
574
- // Batch fetch of prefix search candidates
575
- mockFetch.mockResolvedValueOnce({
576
- ok: true,
577
- json: async () => ({
578
- query: {
579
- pages: {
580
- "2": { title: "Maul (porcine)", revisions: [{ "*": petPageWikitext }] },
581
- "3": { title: "Maul (ranger greatsword skill)", revisions: [{ "*": mainPageWikitext }] },
582
- },
583
- },
584
- }),
585
- });
586
-
587
- // Both IDs share the same bare title "Maul"
588
- const idToTitle = new Map([
589
- [12525, "Maul"],
590
- [41406, "Maul"],
591
- ]);
592
-
593
- const result = await resolveEntityFacts(client, idToTitle, { profession: "Ranger" });
594
-
595
- expect(result.size).toBe(2);
596
- // Weapon skill matched by infobox ID on the main page
597
- expect(result.get(12525).recharge.pve).toBe(4);
598
- // Pet skill: initially got main page facts, then overwritten by prefix search match
599
- expect(result.get(41406).recharge.pve).toBe(10);
600
- });
601
-
602
- test("shares facts across variant IDs when page lists matching infobox ID", async () => {
603
- // "True Nature" has multiple API IDs (one per legend) but one wiki page.
604
- // The wiki lists only one ID in its infobox. All IDs should get the facts,
605
- // and unmatched IDs get queued for prefix search (which finds nothing, so
606
- // they keep the initially-shared facts).
607
- const wikitext = "{{Skill infobox\n| id = 29393\n}}\n{{skill fact|damage|0.5}}\n| recharge = 1";
608
-
609
- mockFetch.mockResolvedValueOnce({
610
- ok: true,
611
- json: async () => ({
612
- query: {
613
- pages: {
614
- "1": { title: "True Nature", revisions: [{ "*": wikitext }] },
615
- },
616
- },
617
- }),
618
- });
619
-
620
- // Prefix search for unmatched IDs — finds nothing useful
621
- mockFetch.mockResolvedValueOnce({
622
- ok: true,
623
- json: async () => ({ query: { prefixsearch: [] } }),
624
- });
625
-
626
- const idToTitle = new Map([
627
- [29393, "True Nature"],
628
- [51675, "True Nature"],
629
- [51714, "True Nature"],
630
- ]);
631
-
632
- const result = await resolveEntityFacts(client, idToTitle);
633
-
634
- // All 3 IDs should have facts (shared from the one wiki page)
635
- expect(result.size).toBe(3);
636
- expect(result.get(29393).recharge.pve).toBe(1);
637
- expect(result.get(51675).recharge.pve).toBe(1);
638
- expect(result.get(51714).recharge.pve).toBe(1);
639
- });
640
-
641
- test("skips pages with no fact templates (keeps API facts)", async () => {
642
- // A wiki page exists but has no {{skill fact|...}} or {{trait fact|...}} templates
643
- const noFactsWikitext = "'''Piercing Shards''' is a trait for Elementalist that makes ice shards pierce.";
644
- mockFetch.mockResolvedValueOnce({
645
- ok: true,
646
- json: async () => ({
647
- query: {
648
- pages: {
649
- "42": { title: "Piercing Shards", revisions: [{ slots: { main: { "*": noFactsWikitext } } }] },
650
- },
651
- },
652
- }),
653
- });
654
-
655
- const idToTitle = new Map([[1234, "Piercing Shards"]]);
656
- const result = await resolveEntityFacts(client, idToTitle);
657
-
658
- // Should NOT have an entry — empty facts would wipe valid API facts
659
- expect(result.size).toBe(0);
660
- expect(result.has(1234)).toBe(false);
661
- });
662
- });
663
-
664
- describe("isDisambiguation", () => {
665
- test("detects {{disambig}} template", () => {
666
- expect(isDisambiguation("Some text\n{{disambig}}\n* [[Link]]")).toBe(true);
667
- });
668
-
669
- test("detects {{disambiguation}} template", () => {
670
- expect(isDisambiguation("{{disambiguation}}\n* [[Link]]")).toBe(true);
671
- });
672
-
673
- test("detects {{disambig|...}} with parameters", () => {
674
- expect(isDisambiguation("{{disambig|skill}}\n* [[Link]]")).toBe(true);
675
- });
676
-
677
- test("returns false for normal skill pages", () => {
678
- expect(isDisambiguation("{{skill fact|damage|0.8}}\n| recharge = 10")).toBe(false);
679
- });
680
-
681
- test("returns false for pages mentioning disambig in prose", () => {
682
- expect(isDisambiguation("This page is not a disambiguation page")).toBe(false);
683
- });
684
- });
685
-
686
- describe("extractInfoboxId", () => {
687
- test("extracts single ID from skill infobox", () => {
688
- const wikitext = "{{Skill infobox\n| id = 5489\n| description = Launch a ball of fire.\n}}";
689
- expect(extractInfoboxId(wikitext)).toEqual([5489]);
690
- });
691
-
692
- test("extracts multi-ID from skill infobox", () => {
693
- const wikitext = "{{Skill infobox\n| id = 5805,6020\n| description = Equip a kit.\n}}";
694
- expect(extractInfoboxId(wikitext)).toEqual([5805, 6020]);
695
- });
696
-
697
- test("extracts ID from trait infobox", () => {
698
- const wikitext = "{{Trait infobox\n| line = Spite\n| id = 903\n}}";
699
- expect(extractInfoboxId(wikitext)).toEqual([903]);
700
- });
701
-
702
- test("returns empty array for location infobox", () => {
703
- const wikitext = "{{Location infobox\n| name = Ring of Fire\n| id = 20\n}}";
704
- expect(extractInfoboxId(wikitext)).toEqual([]);
705
- });
706
-
707
- test("returns empty array for weapon infobox", () => {
708
- const wikitext = "{{Weapon infobox\n| type = Sword\n| id = 29181\n}}";
709
- expect(extractInfoboxId(wikitext)).toEqual([]);
710
- });
711
-
712
- test("returns empty array for page with no infobox", () => {
713
- const wikitext = "'''Some Page''' is about something.";
714
- expect(extractInfoboxId(wikitext)).toEqual([]);
715
- });
716
-
717
- test("handles whitespace variations", () => {
718
- const wikitext = "{{Skill infobox\n|id=5489\n}}";
719
- expect(extractInfoboxId(wikitext)).toEqual([5489]);
720
- });
721
- });