@axiapps/gw2-data 0.1.0 → 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 (43) hide show
  1. package/README.md +156 -0
  2. package/data/overrides.json +24 -0
  3. package/package.json +3 -1
  4. package/src/engine/attributes.js +102 -23
  5. package/src/engine/boons.js +19 -1
  6. package/src/engine/constants.js +27 -2
  7. package/src/engine/index.js +4 -4
  8. package/src/engine/modifiers.js +57 -21
  9. package/src/wiki/parser.js +17 -9
  10. package/scripts/generate-fixtures.js +0 -242
  11. package/tests/api-client.test.js +0 -138
  12. package/tests/cache.test.js +0 -108
  13. package/tests/engine/attributes.test.js +0 -252
  14. package/tests/engine/boons.test.js +0 -129
  15. package/tests/engine/combos.test.js +0 -76
  16. package/tests/engine/constants.test.js +0 -576
  17. package/tests/engine/fixtures/berserker-thief.json +0 -61
  18. package/tests/engine/fixtures/berserker-warrior.json +0 -113
  19. package/tests/engine/fixtures/celestial-firebrand-wvw.json +0 -94
  20. package/tests/engine/fixtures/harrier-druid.json +0 -119
  21. package/tests/engine/fixtures/viper-mirage.json +0 -104
  22. package/tests/engine/graph.test.js +0 -30
  23. package/tests/engine/integration.test.js +0 -111
  24. package/tests/engine/modifiers.test.js +0 -473
  25. package/tests/engine/overrides.test.js +0 -70
  26. package/tests/engine/snapshot.test.js +0 -53
  27. package/tests/engine/test-utils.js +0 -20
  28. package/tests/engine/tooltips.test.js +0 -62
  29. package/tests/fixtures/capture.js +0 -160
  30. package/tests/fixtures/fixtures.json +0 -839
  31. package/tests/integration.test.js +0 -100
  32. package/tests/match.test.js +0 -176
  33. package/tests/merge.test.js +0 -128
  34. package/tests/normalize.test.js +0 -78
  35. package/tests/parser.test.js +0 -506
  36. package/tests/real-data.test.js +0 -296
  37. package/tests/relations.test.js +0 -80
  38. package/tests/resolver.test.js +0 -721
  39. package/tests/validate-live.js +0 -191
  40. package/tests/wiki-client.test.js +0 -468
  41. package/tests/wiki-integration.test.js +0 -177
  42. package/tests/wiki-live-validation.test.js +0 -61
  43. package/tests/wiki-snapshots.test.js +0 -166
@@ -1,191 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
-
4
- /**
5
- * Live validation script — fetches real GW2 API + wiki data and validates
6
- * that our parser + merger produce correct results.
7
- *
8
- * This is NOT a test file. Run it manually to spot-check against live data:
9
- *
10
- * node packages/gw2-data/tests/validate-live.js
11
- * node packages/gw2-data/tests/validate-live.js --ids 5489,30185
12
- * node packages/gw2-data/tests/validate-live.js --type traits --ids 264,1510
13
- *
14
- * Exits with code 1 if any validation fails.
15
- */
16
-
17
- const { WikiClient } = require("../src/wiki/client");
18
- const { Gw2ApiClient } = require("../src/api/client");
19
- const { MemoryCache } = require("../src/wiki/cache");
20
- const { mergeFacts } = require("../src/facts/merge");
21
- const { normalizeFactType } = require("../src/facts/normalize");
22
-
23
- const GW2_API = "https://api.guildwars2.com/v2";
24
- const WIKI_API = "https://wiki.guildwars2.com/api.php";
25
-
26
- // Known split skills from the wiki's balance split category.
27
- // These are good validation targets because they have WvW-specific values.
28
- const DEFAULT_SKILL_IDS = [
29
- 5489, // Fireball
30
- 5536, // Arcane Blast
31
- 30185, // Berserk
32
- 5507, // Meteor Shower
33
- 5548, // Eruption
34
- 5569, // Water Blast
35
- 5503, // Glyph of Storms
36
- 10586, // Mending (Warrior)
37
- 5694, // Burning Retreat (Elementalist)
38
- 10574, // Bull's Charge (Warrior)
39
- ];
40
-
41
- const DEFAULT_TRAIT_IDS = [
42
- 264, // Burning Precision
43
- 1510, // Flow like Water
44
- 1502, // Swift Revenge
45
- 214, // Empowering Might
46
- 1925, // Stalwart Speed
47
- ];
48
-
49
- // ── Helpers ─────────────────────────────────────────────────────────────────
50
-
51
- function color(code, text) {
52
- return `\x1b[${code}m${text}\x1b[0m`;
53
- }
54
- const green = (t) => color(32, t);
55
- const red = (t) => color(31, t);
56
- const yellow = (t) => color(33, t);
57
- const dim = (t) => color(2, t);
58
-
59
- // ── Main ────────────────────────────────────────────────────────────────────
60
-
61
- async function main() {
62
- const args = process.argv.slice(2);
63
- const typeArg = args.includes("--type") ? args[args.indexOf("--type") + 1] : "skills";
64
- const idsArg = args.includes("--ids") ? args[args.indexOf("--ids") + 1] : null;
65
-
66
- const ids = idsArg
67
- ? idsArg.split(",").map((s) => parseInt(s.trim(), 10))
68
- : (typeArg === "traits" ? DEFAULT_TRAIT_IDS : DEFAULT_SKILL_IDS);
69
-
70
- const endpoint = typeArg === "traits" ? "traits" : "skills";
71
-
72
- const wikiClient = new WikiClient({
73
- cache: new MemoryCache(),
74
- fetch: globalThis.fetch,
75
- });
76
-
77
- const apiClient = new Gw2ApiClient({
78
- cache: new MemoryCache(),
79
- fetch: globalThis.fetch,
80
- });
81
-
82
- console.log(`\nValidating ${ids.length} ${endpoint} against live GW2 data...\n`);
83
-
84
- let passed = 0;
85
- let failed = 0;
86
- let skipped = 0;
87
-
88
- for (const id of ids) {
89
- process.stdout.write(` ${id} `);
90
-
91
- // 1. Fetch API data
92
- let apiData;
93
- try {
94
- const url = `${GW2_API}/${endpoint}/${id}?lang=en`;
95
- apiData = await apiClient.fetchJson(url);
96
- } catch (err) {
97
- console.log(yellow(`SKIP — API error: ${err.message}`));
98
- skipped++;
99
- continue;
100
- }
101
-
102
- const name = apiData.name || `(${endpoint} ${id})`;
103
- process.stdout.write(`${name.padEnd(30)} `);
104
-
105
- // 2. Fetch wiki page
106
- let wikitext;
107
- try {
108
- wikitext = await wikiClient.getWikitext(name);
109
- } catch (err) {
110
- console.log(yellow(`SKIP — wiki error: ${err.message}`));
111
- skipped++;
112
- continue;
113
- }
114
-
115
- if (!wikitext) {
116
- console.log(yellow("SKIP — no wiki page"));
117
- skipped++;
118
- continue;
119
- }
120
-
121
- // 3. Parse wiki facts
122
- const wikiResult = wikiClient.parseFacts(wikitext);
123
- const apiFacts = apiData.facts || [];
124
-
125
- // 4. Validate
126
- const issues = [];
127
-
128
- // Check: if wiki has a split, we should get split facts
129
- if (wikiResult.splitGrouping?.wvwHasSplit && wikiResult.facts.length === 0) {
130
- issues.push("split detected but no WvW facts parsed");
131
- }
132
-
133
- // Check: wiki fact types should all be recognized API types
134
- for (const fact of wikiResult.facts) {
135
- const normalized = normalizeFactType(fact.type);
136
- if (!normalized) {
137
- issues.push(`unknown fact type: ${fact.type}`);
138
- }
139
- }
140
-
141
- // Check: merge should not crash
142
- try {
143
- const merged = mergeFacts(apiFacts, wikiResult.facts, { complete: false });
144
- if (wikiResult.facts.length > 0 && merged.length === 0) {
145
- issues.push("merge produced empty result from non-empty inputs");
146
- }
147
- } catch (err) {
148
- issues.push(`merge crashed: ${err.message}`);
149
- }
150
-
151
- // Check: damage coefficients should be reasonable (0 < coeff < 50)
152
- for (const fact of wikiResult.facts) {
153
- if (fact.type === "Damage" && fact.dmg_multiplier !== undefined) {
154
- if (fact.dmg_multiplier < 0 || fact.dmg_multiplier > 50) {
155
- issues.push(`suspicious damage coefficient: ${fact.dmg_multiplier}`);
156
- }
157
- }
158
- }
159
-
160
- // Check: durations should be non-negative
161
- for (const fact of wikiResult.facts) {
162
- if (fact.duration !== undefined && fact.duration < 0) {
163
- issues.push(`negative duration: ${fact.duration}`);
164
- }
165
- }
166
-
167
- if (issues.length > 0) {
168
- console.log(red("FAIL"));
169
- for (const issue of issues) {
170
- console.log(` ${red("✗")} ${issue}`);
171
- }
172
- failed++;
173
- } else {
174
- const splitLabel = wikiResult.splitGrouping?.wvwHasSplit ? "split" : "universal";
175
- console.log(
176
- green("OK") +
177
- dim(` (${wikiResult.facts.length} wiki facts, ${apiFacts.length} API facts, ${splitLabel})`)
178
- );
179
- passed++;
180
- }
181
- }
182
-
183
- console.log(`\n ${green(`${passed} passed`)}, ${failed > 0 ? red(`${failed} failed`) : `${failed} failed`}, ${skipped} skipped\n`);
184
-
185
- if (failed > 0) process.exit(1);
186
- }
187
-
188
- main().catch((err) => {
189
- console.error(err);
190
- process.exit(1);
191
- });
@@ -1,468 +0,0 @@
1
- "use strict";
2
-
3
- const { WikiClient } = require("../src/wiki/client");
4
- const { MemoryCache } = require("../src/wiki/cache");
5
-
6
- describe("WikiClient", () => {
7
- let client;
8
- let mockFetch;
9
-
10
- beforeEach(() => {
11
- mockFetch = jest.fn();
12
- client = new WikiClient({
13
- cache: new MemoryCache(),
14
- fetch: mockFetch,
15
- });
16
- });
17
-
18
- describe("getWikitext", () => {
19
- test("fetches and returns raw wikitext for a page", async () => {
20
- mockFetch.mockResolvedValueOnce({
21
- ok: true,
22
- json: async () => ({
23
- query: {
24
- pages: {
25
- "123": {
26
- title: "Fireball",
27
- revisions: [{ "*": "{{skill infobox\n| id = 5489\n}}" }],
28
- },
29
- },
30
- },
31
- }),
32
- });
33
-
34
- const result = await client.getWikitext("Fireball");
35
- expect(result).toBe("{{skill infobox\n| id = 5489\n}}");
36
- expect(mockFetch).toHaveBeenCalledTimes(1);
37
- expect(mockFetch.mock.calls[0][0]).toContain("action=query");
38
- expect(mockFetch.mock.calls[0][0]).toContain("rvprop=content");
39
- });
40
-
41
- test("returns null for missing page", async () => {
42
- mockFetch.mockResolvedValueOnce({
43
- ok: true,
44
- json: async () => ({
45
- query: {
46
- pages: {
47
- "-1": { missing: true },
48
- },
49
- },
50
- }),
51
- });
52
-
53
- const result = await client.getWikitext("Nonexistent");
54
- expect(result).toBeNull();
55
- });
56
-
57
- test("caches wikitext on subsequent calls", async () => {
58
- mockFetch.mockResolvedValueOnce({
59
- ok: true,
60
- json: async () => ({
61
- query: {
62
- pages: {
63
- "123": {
64
- title: "Fireball",
65
- revisions: [{ "*": "wikitext content" }],
66
- },
67
- },
68
- },
69
- }),
70
- });
71
-
72
- await client.getWikitext("Fireball");
73
- const result2 = await client.getWikitext("Fireball");
74
- expect(result2).toBe("wikitext content");
75
- expect(mockFetch).toHaveBeenCalledTimes(1);
76
- });
77
- });
78
-
79
- describe("getWikitextBatch", () => {
80
- test("fetches multiple pages in a single request", async () => {
81
- mockFetch.mockResolvedValueOnce({
82
- ok: true,
83
- json: async () => ({
84
- query: {
85
- pages: {
86
- "1": { title: "Fireball", revisions: [{ "*": "fireball wikitext" }] },
87
- "2": { title: "Shelter", revisions: [{ "*": "shelter wikitext" }] },
88
- "3": { title: "Moa Stance", revisions: [{ "*": "moa wikitext" }] },
89
- },
90
- },
91
- }),
92
- });
93
-
94
- const result = await client.getWikitextBatch(["Fireball", "Shelter", "Moa Stance"]);
95
- expect(result.size).toBe(3);
96
- expect(result.get("Fireball")).toBe("fireball wikitext");
97
- expect(result.get("Shelter")).toBe("shelter wikitext");
98
- expect(result.get("Moa Stance")).toBe("moa wikitext");
99
- expect(mockFetch).toHaveBeenCalledTimes(1);
100
- });
101
-
102
- test("chunks at 50 titles per request", async () => {
103
- // Generate 51 titles
104
- const titles = Array.from({ length: 51 }, (_, i) => `Skill_${i}`);
105
-
106
- const makeBatchResponse = (batch) => ({
107
- ok: true,
108
- json: async () => ({
109
- query: {
110
- pages: Object.fromEntries(
111
- batch.map((t, i) => [String(i), { title: t, revisions: [{ "*": `${t} text` }] }])
112
- ),
113
- },
114
- }),
115
- });
116
-
117
- // First call: 50 titles, second call: 1 title
118
- mockFetch
119
- .mockResolvedValueOnce(makeBatchResponse(titles.slice(0, 50)))
120
- .mockResolvedValueOnce(makeBatchResponse(titles.slice(50)));
121
-
122
- const result = await client.getWikitextBatch(titles);
123
- expect(result.size).toBe(51);
124
- expect(mockFetch).toHaveBeenCalledTimes(2);
125
- expect(result.get("Skill_0")).toBe("Skill_0 text");
126
- expect(result.get("Skill_50")).toBe("Skill_50 text");
127
- });
128
-
129
- test("uses cached entries without fetching", async () => {
130
- // Pre-populate cache
131
- client._cache.set("wikitext:Fireball", "cached fireball", 60000);
132
-
133
- mockFetch.mockResolvedValueOnce({
134
- ok: true,
135
- json: async () => ({
136
- query: {
137
- pages: {
138
- "1": { title: "Shelter", revisions: [{ "*": "shelter text" }] },
139
- },
140
- },
141
- }),
142
- });
143
-
144
- const result = await client.getWikitextBatch(["Fireball", "Shelter"]);
145
- expect(result.get("Fireball")).toBe("cached fireball");
146
- expect(result.get("Shelter")).toBe("shelter text");
147
- expect(mockFetch).toHaveBeenCalledTimes(1);
148
- // URL should only contain Shelter
149
- expect(mockFetch.mock.calls[0][0]).toContain("Shelter");
150
- expect(mockFetch.mock.calls[0][0]).not.toContain("Fireball");
151
- });
152
-
153
- test("returns null for missing pages", async () => {
154
- mockFetch.mockResolvedValueOnce({
155
- ok: true,
156
- json: async () => ({
157
- query: {
158
- pages: {
159
- "-1": { title: "Nonexistent Skill", missing: true },
160
- "1": { title: "Fireball", revisions: [{ "*": "fireball text" }] },
161
- },
162
- },
163
- }),
164
- });
165
-
166
- const result = await client.getWikitextBatch(["Fireball", "Nonexistent Skill"]);
167
- expect(result.get("Fireball")).toBe("fireball text");
168
- expect(result.get("Nonexistent Skill")).toBe(null);
169
- });
170
-
171
- test("handles failed HTTP response gracefully", async () => {
172
- mockFetch.mockResolvedValueOnce({ ok: false, status: 500 });
173
-
174
- const result = await client.getWikitextBatch(["Fireball", "Shelter"]);
175
- expect(result.get("Fireball")).toBe(null);
176
- expect(result.get("Shelter")).toBe(null);
177
- });
178
-
179
- test("returns empty map for empty input", async () => {
180
- const result = await client.getWikitextBatch([]);
181
- expect(result.size).toBe(0);
182
- expect(mockFetch).not.toHaveBeenCalled();
183
- });
184
-
185
- test("caches missing pages so subsequent batch calls skip them", async () => {
186
- mockFetch.mockResolvedValueOnce({
187
- ok: true,
188
- json: async () => ({
189
- query: {
190
- pages: {
191
- "-1": { title: "Missing Skill", missing: true },
192
- "1": { title: "Fireball", revisions: [{ "*": "fireball text" }] },
193
- },
194
- },
195
- }),
196
- });
197
-
198
- const result1 = await client.getWikitextBatch(["Fireball", "Missing Skill"]);
199
- expect(result1.get("Missing Skill")).toBeNull();
200
- expect(result1.get("Fireball")).toBe("fireball text");
201
-
202
- // Second batch: "Missing Skill" should come from cache, only "Shelter" fetched
203
- mockFetch.mockResolvedValueOnce({
204
- ok: true,
205
- json: async () => ({
206
- query: {
207
- pages: {
208
- "1": { title: "Shelter", revisions: [{ "*": "shelter text" }] },
209
- },
210
- },
211
- }),
212
- });
213
-
214
- const result2 = await client.getWikitextBatch(["Missing Skill", "Shelter"]);
215
- expect(result2.get("Missing Skill")).toBeNull();
216
- expect(result2.get("Shelter")).toBe("shelter text");
217
- expect(mockFetch).toHaveBeenCalledTimes(2);
218
- // Second fetch should only contain Shelter, not Missing Skill
219
- expect(mockFetch.mock.calls[1][0]).toContain("Shelter");
220
- expect(mockFetch.mock.calls[1][0]).not.toContain("Missing");
221
- });
222
-
223
- test("handles MediaWiki title normalization", async () => {
224
- mockFetch.mockResolvedValueOnce({
225
- ok: true,
226
- json: async () => ({
227
- query: {
228
- normalized: [{ from: "fireball", to: "Fireball" }],
229
- pages: {
230
- "1": { title: "Fireball", revisions: [{ "*": "fireball text" }] },
231
- },
232
- },
233
- }),
234
- });
235
-
236
- const result = await client.getWikitextBatch(["fireball"]);
237
- expect(result.get("fireball")).toBe("fireball text");
238
- });
239
- });
240
-
241
- describe("getRecentChanges", () => {
242
- test("returns list of recently changed page titles", async () => {
243
- mockFetch.mockResolvedValueOnce({
244
- ok: true,
245
- json: async () => ({
246
- query: {
247
- recentchanges: [
248
- { title: "Fireball", timestamp: "2026-04-09T10:00:00Z" },
249
- { title: "Ice Spike", timestamp: "2026-04-09T09:00:00Z" },
250
- ],
251
- },
252
- }),
253
- });
254
-
255
- const changes = await client.getRecentChanges("2026-04-08T00:00:00Z");
256
- expect(changes).toEqual(["Fireball", "Ice Spike"]);
257
- });
258
- });
259
-
260
- describe("getWikitext", () => {
261
- test("caches missing pages so subsequent calls skip the network", async () => {
262
- mockFetch.mockResolvedValueOnce({
263
- ok: true,
264
- json: async () => ({
265
- query: {
266
- pages: {
267
- "-1": { missing: true },
268
- },
269
- },
270
- }),
271
- });
272
-
273
- const result1 = await client.getWikitext("Nonexistent");
274
- expect(result1).toBeNull();
275
-
276
- // Second call should NOT hit the network
277
- const result2 = await client.getWikitext("Nonexistent");
278
- expect(result2).toBeNull();
279
- expect(mockFetch).toHaveBeenCalledTimes(1);
280
- });
281
-
282
- test("returns null on HTTP error", async () => {
283
- mockFetch.mockResolvedValueOnce({ ok: false, status: 500 });
284
- const result = await client.getWikitext("Fireball");
285
- expect(result).toBeNull();
286
- });
287
-
288
- test("returns null when query.pages is missing", async () => {
289
- mockFetch.mockResolvedValueOnce({
290
- ok: true,
291
- json: async () => ({ query: {} }),
292
- });
293
- const result = await client.getWikitext("Fireball");
294
- expect(result).toBeNull();
295
- });
296
- });
297
-
298
- describe("getRecentChanges", () => {
299
- test("returns empty array on HTTP error", async () => {
300
- mockFetch.mockResolvedValueOnce({ ok: false, status: 500 });
301
- const result = await client.getRecentChanges("2026-04-08T00:00:00Z");
302
- expect(result).toEqual([]);
303
- });
304
-
305
- test("deduplicates changed page titles", async () => {
306
- mockFetch.mockResolvedValueOnce({
307
- ok: true,
308
- json: async () => ({
309
- query: {
310
- recentchanges: [
311
- { title: "Fireball", timestamp: "2026-04-09T10:00:00Z" },
312
- { title: "Fireball", timestamp: "2026-04-09T09:00:00Z" },
313
- { title: "Ice Spike", timestamp: "2026-04-09T08:00:00Z" },
314
- ],
315
- },
316
- }),
317
- });
318
- const result = await client.getRecentChanges("2026-04-08T00:00:00Z");
319
- expect(result).toEqual(["Fireball", "Ice Spike"]);
320
- });
321
- });
322
-
323
- describe("refresh", () => {
324
- test("first call records timestamp and returns empty array", async () => {
325
- const result = await client.refresh();
326
- expect(result).toEqual([]);
327
- });
328
-
329
- test("second call fetches recent changes and invalidates cache", async () => {
330
- // First call sets the timestamp
331
- await client.refresh();
332
-
333
- // Pre-populate cache
334
- const cache = client._cache;
335
- cache.set("wikitext:Fireball", "old content", 60000);
336
- cache.set("facts:Fireball", [{ type: "Damage" }], 60000);
337
-
338
- mockFetch.mockResolvedValueOnce({
339
- ok: true,
340
- json: async () => ({
341
- query: {
342
- recentchanges: [
343
- { title: "Fireball", timestamp: "2026-04-09T10:00:00Z" },
344
- ],
345
- },
346
- }),
347
- });
348
-
349
- const changed = await client.refresh();
350
- expect(changed).toEqual(["Fireball"]);
351
- expect(cache.get("wikitext:Fireball")).toBeNull();
352
- expect(cache.get("facts:Fireball")).toBeNull();
353
- });
354
- });
355
-
356
- describe("rate limiting", () => {
357
- test("delays subsequent requests within rate limit window", async () => {
358
- const response = {
359
- ok: true,
360
- json: async () => ({
361
- query: {
362
- pages: { "1": { title: "A", revisions: [{ "*": "a" }] } },
363
- },
364
- }),
365
- };
366
- mockFetch.mockResolvedValue(response);
367
-
368
- const start = Date.now();
369
- await client.getWikitext("A");
370
- await client.getWikitext("B"); // different key, forces second fetch
371
- const elapsed = Date.now() - start;
372
-
373
- // Should have waited ~200ms between requests
374
- expect(elapsed).toBeGreaterThanOrEqual(150);
375
- expect(mockFetch).toHaveBeenCalledTimes(2);
376
- });
377
- });
378
-
379
- describe("prefixSearch", () => {
380
- test("returns matching page titles", async () => {
381
- mockFetch.mockResolvedValueOnce({
382
- ok: true,
383
- json: async () => ({
384
- query: {
385
- prefixsearch: [
386
- { title: "Ring of Fire (elementalist skill)" },
387
- { title: "Ring of Fire (location)" },
388
- ],
389
- },
390
- }),
391
- });
392
-
393
- const results = await client.prefixSearch("Ring of Fire (");
394
- expect(results).toEqual([
395
- "Ring of Fire (elementalist skill)",
396
- "Ring of Fire (location)",
397
- ]);
398
- });
399
-
400
- test("returns empty array when no matches", async () => {
401
- mockFetch.mockResolvedValueOnce({
402
- ok: true,
403
- json: async () => ({
404
- query: { prefixsearch: [] },
405
- }),
406
- });
407
-
408
- const results = await client.prefixSearch("Nonexistent (");
409
- expect(results).toEqual([]);
410
- });
411
-
412
- test("returns empty array on fetch failure", async () => {
413
- mockFetch.mockResolvedValueOnce({ ok: false });
414
-
415
- const results = await client.prefixSearch("Test (");
416
- expect(results).toEqual([]);
417
- });
418
- });
419
-
420
- describe("parseFacts", () => {
421
- test("parses wikitext into facts for a game mode", () => {
422
- const wikitext = [
423
- "{{skill infobox",
424
- "| id = 5489",
425
- "| split = pve, wvw, pvp",
426
- "}}",
427
- "{{skill fact|damage|0.8|game mode=wvw}}",
428
- "{{skill fact|damage|1.2|game mode=pve}}",
429
- "{{skill fact|recharge|25}}",
430
- ].join("\n");
431
-
432
- const result = client.parseFacts(wikitext);
433
- expect(result.facts.length).toBeGreaterThanOrEqual(2);
434
- const damageFact = result.facts.find((f) => f.type === "Damage");
435
- expect(damageFact.dmg_multiplier).toBe(0.8);
436
- const rechargeFact = result.facts.find((f) => f.type === "Recharge");
437
- expect(rechargeFact.value).toBe(25);
438
- });
439
-
440
- test("falls back to parseInfoboxParams when no skill fact templates found", () => {
441
- const wikitext = [
442
- "{{skill infobox",
443
- "| id = 1234",
444
- "| split = pve, wvw, pvp",
445
- "| recharge wvw = 30",
446
- "}}",
447
- ].join("\n");
448
-
449
- const result = client.parseFacts(wikitext);
450
- expect(result.splitGrouping.wvwHasSplit).toBe(true);
451
- const rechargeFact = result.facts.find((f) => f.type === "Recharge");
452
- expect(rechargeFact).toBeTruthy();
453
- expect(rechargeFact.value).toBe(30);
454
- });
455
-
456
- test("returns null splitGrouping when no split field present", () => {
457
- const wikitext = [
458
- "{{skill infobox",
459
- "| id = 1234",
460
- "}}",
461
- "{{skill fact|damage|1.0}}",
462
- ].join("\n");
463
-
464
- const result = client.parseFacts(wikitext);
465
- expect(result.splitGrouping).toBeNull();
466
- });
467
- });
468
- });