@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.
- package/data/overrides.json +24 -0
- package/package.json +3 -1
- package/src/engine/attributes.js +102 -23
- package/src/engine/boons.js +19 -1
- package/src/engine/constants.js +27 -2
- package/src/engine/index.js +4 -4
- package/src/engine/modifiers.js +57 -21
- package/src/wiki/parser.js +17 -9
- package/scripts/generate-fixtures.js +0 -242
- package/tests/api-client.test.js +0 -138
- package/tests/cache.test.js +0 -108
- package/tests/engine/attributes.test.js +0 -252
- package/tests/engine/boons.test.js +0 -129
- package/tests/engine/combos.test.js +0 -76
- package/tests/engine/constants.test.js +0 -576
- package/tests/engine/fixtures/berserker-thief.json +0 -61
- package/tests/engine/fixtures/berserker-warrior.json +0 -113
- package/tests/engine/fixtures/celestial-firebrand-wvw.json +0 -94
- package/tests/engine/fixtures/harrier-druid.json +0 -119
- package/tests/engine/fixtures/viper-mirage.json +0 -104
- package/tests/engine/graph.test.js +0 -30
- package/tests/engine/integration.test.js +0 -111
- package/tests/engine/modifiers.test.js +0 -473
- package/tests/engine/overrides.test.js +0 -70
- package/tests/engine/snapshot.test.js +0 -53
- package/tests/engine/test-utils.js +0 -20
- package/tests/engine/tooltips.test.js +0 -62
- package/tests/fixtures/capture.js +0 -160
- package/tests/fixtures/fixtures.json +0 -839
- package/tests/integration.test.js +0 -100
- package/tests/match.test.js +0 -176
- package/tests/merge.test.js +0 -128
- package/tests/normalize.test.js +0 -78
- package/tests/parser.test.js +0 -506
- package/tests/real-data.test.js +0 -296
- package/tests/relations.test.js +0 -80
- package/tests/resolver.test.js +0 -721
- package/tests/validate-live.js +0 -191
- package/tests/wiki-client.test.js +0 -468
- package/tests/wiki-integration.test.js +0 -177
- package/tests/wiki-live-validation.test.js +0 -61
- package/tests/wiki-snapshots.test.js +0 -166
package/tests/resolver.test.js
DELETED
|
@@ -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
|
-
});
|