@thefehr/foundry-playwright 0.2.1

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 (48) hide show
  1. package/README.md +79 -0
  2. package/dist/auth.d.ts +18 -0
  3. package/dist/auth.js +287 -0
  4. package/dist/canvas.d.ts +47 -0
  5. package/dist/canvas.js +105 -0
  6. package/dist/cli/index.d.ts +2 -0
  7. package/dist/cli/index.js +83 -0
  8. package/dist/cli/init.d.ts +5 -0
  9. package/dist/cli/init.js +129 -0
  10. package/dist/deprecations.d.ts +24 -0
  11. package/dist/deprecations.js +59 -0
  12. package/dist/docker.d.ts +37 -0
  13. package/dist/docker.js +140 -0
  14. package/dist/fixtures.d.ts +29 -0
  15. package/dist/fixtures.js +112 -0
  16. package/dist/helpers.d.ts +100 -0
  17. package/dist/helpers.js +414 -0
  18. package/dist/index.d.ts +9 -0
  19. package/dist/index.js +9 -0
  20. package/dist/setup/base.d.ts +97 -0
  21. package/dist/setup/base.js +53 -0
  22. package/dist/setup/index.d.ts +14 -0
  23. package/dist/setup/index.js +126 -0
  24. package/dist/setup/v13.d.ts +28 -0
  25. package/dist/setup/v13.js +308 -0
  26. package/dist/setup/v14.d.ts +31 -0
  27. package/dist/setup/v14.js +421 -0
  28. package/dist/state.d.ts +139 -0
  29. package/dist/state.js +321 -0
  30. package/dist/systems/base.d.ts +48 -0
  31. package/dist/systems/base.js +57 -0
  32. package/dist/systems/dnd5e.d.ts +27 -0
  33. package/dist/systems/dnd5e.js +30 -0
  34. package/dist/systems/index.d.ts +13 -0
  35. package/dist/systems/index.js +20 -0
  36. package/dist/systems/pf2e.d.ts +25 -0
  37. package/dist/systems/pf2e.js +62 -0
  38. package/dist/types/index.d.ts +18 -0
  39. package/dist/types/index.js +11 -0
  40. package/dist/ui/base.d.ts +35 -0
  41. package/dist/ui/base.js +43 -0
  42. package/dist/ui/dnd5e.d.ts +8 -0
  43. package/dist/ui/dnd5e.js +10 -0
  44. package/dist/ui/index.d.ts +45 -0
  45. package/dist/ui/index.js +72 -0
  46. package/dist/ui/tidy5e.d.ts +11 -0
  47. package/dist/ui/tidy5e.js +30 -0
  48. package/package.json +67 -0
@@ -0,0 +1,414 @@
1
+ import { expect } from "@playwright/test";
2
+ import path from "path";
3
+ import fs from "fs";
4
+ /**
5
+ * Loads the verification registry from verified-versions.json.
6
+ */
7
+ export function getVerificationRegistry() {
8
+ try {
9
+ const registryPath = path.join(process.cwd(), "verified-versions.json");
10
+ if (fs.existsSync(registryPath)) {
11
+ return JSON.parse(fs.readFileSync(registryPath, "utf8"));
12
+ }
13
+ }
14
+ catch (e) {
15
+ console.warn("[getVerificationRegistry] Failed to load registry:", e);
16
+ }
17
+ return [];
18
+ }
19
+ /**
20
+ * Helper to automatically set up a Foundry VTT instance before all tests in a file.
21
+ * Reduces boilerplate in spec files.
22
+ * @param test The Playwright test object.
23
+ * @param config Foundry setup configuration.
24
+ */
25
+ export function useFoundry(test, config = {}) {
26
+ test.beforeAll(async ({ browser }) => {
27
+ const page = await browser.newPage();
28
+ const { foundrySetup } = await import("./auth.js");
29
+ await foundrySetup(page, config);
30
+ await page.close();
31
+ });
32
+ }
33
+ /**
34
+ * Validates the current Foundry/System/Module stack against the registry.
35
+ */
36
+ export async function validateStack(page, targetVersion) {
37
+ const registry = getVerificationRegistry();
38
+ if (registry.length === 0)
39
+ return;
40
+ const current = await page.evaluate(() => {
41
+ const g = window.game;
42
+ const foundry = g.version || g.release?.generation;
43
+ const system = g.system.id;
44
+ const systemVersion = g.system.version;
45
+ const modules = Array.from(g.modules.values())
46
+ .filter((m) => m.active && m.id !== "fake-module")
47
+ .map((m) => ({ id: m.id, version: m.version }));
48
+ return { foundry, system, systemVersion, modules };
49
+ });
50
+ const fvtt = String(targetVersion || current.foundry);
51
+ // Find entries for this FVTT + System
52
+ const matches = registry.filter((e) => fvtt.startsWith(e.fvtt) && e.system === current.system);
53
+ if (matches.length === 0) {
54
+ console.warn(`\n[VALIDATION] ⚠️ Untested Stack: FVTT ${fvtt} + ${current.system} is not in the verified registry.`);
55
+ return "untested";
56
+ }
57
+ // Check for specific version match or incompatibility
58
+ const exactMatch = matches.find((m) => m.systemVersion === current.systemVersion);
59
+ if (exactMatch) {
60
+ if (exactMatch.status === "incompatible") {
61
+ console.error(`\n[VALIDATION] ❌ INCOMPATIBLE STACK DETECTED!\n` +
62
+ `FVTT ${fvtt} + ${current.system} v${current.systemVersion} is marked as INCOMPATIBLE.\n` +
63
+ `Notes: ${exactMatch.notes}\n`);
64
+ return "incompatible";
65
+ }
66
+ console.log(`[VALIDATION] ✅ Stable Stack: FVTT ${fvtt} + ${current.system} v${current.systemVersion} is verified.`);
67
+ return "stable";
68
+ }
69
+ console.warn(`\n[VALIDATION] ⚠️ Untested Version: FVTT ${fvtt} + ${current.system} is verified, but not with version v${current.systemVersion}.`);
70
+ return "untested";
71
+ }
72
+ /**
73
+ * Aggressively removes Foundry VTT tours and overlays from the DOM and localStorage.
74
+ * @param page The Playwright Page object.
75
+ */
76
+ export async function disableTour(page) {
77
+ const removalScript = () => {
78
+ const selectors = [
79
+ ".tour-overlay",
80
+ "#tour-overlay",
81
+ ".joyride-overlay",
82
+ ".foundry-tour-overlay",
83
+ ".tour-dot",
84
+ ".tour-step-anchor",
85
+ ".nue-overlay",
86
+ ".nue-container",
87
+ "foundry-guide",
88
+ ];
89
+ // 1. Remove elements from DOM
90
+ const remove = () => {
91
+ selectors.forEach((s) => {
92
+ document.querySelectorAll(s).forEach((el) => {
93
+ el.style.display = "none";
94
+ el.style.pointerEvents = "none";
95
+ el.remove();
96
+ });
97
+ });
98
+ // Also remove pointer-events from body if blocked
99
+ if (document.body.classList.contains("tour-open") ||
100
+ document.body.style.pointerEvents === "none") {
101
+ document.body.classList.remove("tour-open", "nue-open");
102
+ document.body.style.pointerEvents = "auto";
103
+ }
104
+ };
105
+ remove();
106
+ // 2. Inject override style
107
+ if (!document.getElementById("foundry-playwright-no-tour")) {
108
+ const style = document.createElement("style");
109
+ style.id = "foundry-playwright-no-tour";
110
+ style.innerHTML = `
111
+ .tour-overlay, #tour-overlay, .joyride-overlay, .foundry-tour-overlay, .nue-overlay, .nue-container, foundry-guide, .tour-step-anchor, .tour-dot {
112
+ display: none !important;
113
+ visibility: hidden !important;
114
+ pointer-events: none !important;
115
+ }
116
+ `;
117
+ document.head.appendChild(style);
118
+ }
119
+ // 3. Set localStorage to mark tours as completed
120
+ try {
121
+ const tourProgress = { core: { backupsOverview: 1, welcome: 1, setup: 1 } };
122
+ window.localStorage.setItem("core.tourProgress", JSON.stringify(tourProgress));
123
+ }
124
+ catch { }
125
+ // 4. Mutation Observer to keep them gone
126
+ const observer = new MutationObserver(remove);
127
+ observer.observe(document.body, { childList: true, subtree: true });
128
+ };
129
+ // Run as init script for new pages
130
+ await page.addInitScript(removalScript);
131
+ // Run immediately on the current page
132
+ await page.evaluate(removalScript).catch(() => null);
133
+ }
134
+ /**
135
+ * Navigates to a specific tab by name or ID.
136
+ * @param page The Playwright Page object.
137
+ * @param tabName The logical name or data-tab value of the tab.
138
+ */
139
+ export async function switchTab(page, tabName) {
140
+ // Map logical names to data-tab values for robustness
141
+ const tabMap = {
142
+ "Game Worlds": "worlds",
143
+ Worlds: "worlds",
144
+ "Game Systems": "systems",
145
+ Systems: "systems",
146
+ "Add-on Modules": "modules",
147
+ Modules: "modules",
148
+ Configuration: "config",
149
+ "Update Software": "update",
150
+ };
151
+ const dataTabName = tabMap[tabName] || tabName.toLowerCase();
152
+ console.log(`[switchTab] Switching to tab: ${tabName}`);
153
+ // 1. Try robust data-tab selectors first (V13/V14 common patterns)
154
+ const selectors = [
155
+ `[data-tab="${dataTabName}"]`,
156
+ `[data-action="tab"][data-tab="${dataTabName}"]`,
157
+ `nav.tabs [data-tab="${dataTabName}"]`,
158
+ `nav.tabs [data-action="tab"][data-tab="${dataTabName}"]`,
159
+ `button[role="tab"][data-tab="${dataTabName}"]`,
160
+ `[data-application-part] [data-tab="${dataTabName}"]`,
161
+ // Fallbacks if mapping failed but text matches
162
+ `nav.tabs [data-tab]:has-text("${tabName}")`,
163
+ `[data-action="tab"]:has-text("${tabName}")`,
164
+ `.tabs .item:has-text("${tabName}")`,
165
+ `button[role="tab"]:has-text("${tabName}")`,
166
+ ];
167
+ let tab = null;
168
+ for (const selector of selectors) {
169
+ const candidate = page.locator(selector).first();
170
+ if ((await candidate.count()) > 0 && (await candidate.isVisible())) {
171
+ console.log(`[switchTab] Found candidate with selector: ${selector}`);
172
+ tab = candidate;
173
+ break;
174
+ }
175
+ }
176
+ if (!tab) {
177
+ console.log(`[switchTab] No robust selector matched for "${tabName}". Falling back to text search.`);
178
+ tab = page
179
+ .locator(`*:visible:has-text("${tabName}")`)
180
+ .filter({ hasNot: page.locator("option") })
181
+ .first();
182
+ }
183
+ await expect(tab).toBeVisible({ timeout: 15000 });
184
+ // Get the data-tab attribute if it exists to wait for content later
185
+ const dataTab = (await tab.getAttribute("data-tab")) || (await tab.getAttribute("data-action")) || dataTabName;
186
+ // Force click via evaluate to bypass overlays, then wait for transition
187
+ await tab.evaluate((el) => el.click());
188
+ // Wait for the tab to be active or for the target content to be visible
189
+ await page
190
+ .waitForFunction(({ name, dataTab }) => {
191
+ const activeTab = document.querySelector(".tabs .item.active, [data-action='tab'].active, [role='tab'][aria-selected='true'], .active[data-tab], .tab.active, [data-application-part].active, nav h2.active, .navigation h2.active, h2.active");
192
+ const isTabActive = activeTab?.textContent?.trim().includes(name) ||
193
+ (dataTab && activeTab?.getAttribute("data-tab") === dataTab);
194
+ // Also check if a section with that data-tab is now visible
195
+ const contentVisible = dataTab
196
+ ? document.querySelector(`section.tab[data-tab="${dataTab}"].active, .tab[data-tab="${dataTab}"].active, [data-application-part="${dataTab}"].active`) !== null ||
197
+ document.querySelector(`#setup-packages-${dataTab}.active`) !== null ||
198
+ document.querySelector(`#setup-packages-${dataTab}:not([style*="display: none"])`) !==
199
+ null
200
+ : true;
201
+ return isTabActive || contentVisible;
202
+ }, { name: tabName, dataTab }, { timeout: 10000 })
203
+ .catch(() => null);
204
+ console.log(`[switchTab] Tab "${tabName}" clicked and verified.`);
205
+ }
206
+ /**
207
+ * Navigates to the Systems tab and opens the installation dialog.
208
+ * @param page The Playwright Page object.
209
+ */
210
+ export async function openSystemInstallDialog(page) {
211
+ const { getSetupAdapter } = await import("./setup/index.js");
212
+ const adapter = await getSetupAdapter(page);
213
+ return await adapter.openSystemInstallDialog(page);
214
+ }
215
+ /**
216
+ * Navigates to the Modules tab and opens the installation dialog.
217
+ * @param page The Playwright Page object.
218
+ */
219
+ export async function openModuleInstallDialog(page) {
220
+ const { getSetupAdapter } = await import("./setup/index.js");
221
+ const adapter = await getSetupAdapter(page);
222
+ return await adapter.openModuleInstallDialog(page);
223
+ }
224
+ /**
225
+ * Installs a system from a manifest URL.
226
+ * @param page The Playwright Page object.
227
+ * @param manifestUrl The URL to the system.json manifest.
228
+ */
229
+ export async function installSystemFromManifest(page, manifestUrl) {
230
+ console.log(`[installSystemFromManifest] Installing from: ${manifestUrl}`);
231
+ const dialog = await openSystemInstallDialog(page);
232
+ await installFromManifest(page, dialog, manifestUrl);
233
+ }
234
+ /**
235
+ * Installs a module from a manifest URL.
236
+ * @param page The Playwright Page object.
237
+ * @param manifestUrl The URL to the module.json manifest.
238
+ */
239
+ export async function installModuleFromManifest(page, manifestUrl) {
240
+ console.log(`[installModuleFromManifest] Installing from: ${manifestUrl}`);
241
+ const dialog = await openModuleInstallDialog(page);
242
+ await installFromManifest(page, dialog, manifestUrl);
243
+ }
244
+ /**
245
+ * Shared helper for filling manifest URL and clicking install in a dialog.
246
+ */
247
+ async function installFromManifest(page, dialog, manifestUrl) {
248
+ // Foundry's manifest input is usually at the bottom
249
+ const manifestInput = dialog
250
+ .locator('input#install-package-manifestUrl, input[name="manifestURL"], input:not(#world-filter):not(#system-filter):not(#module-filter):not([type="checkbox"]):not([type="radio"])')
251
+ .first();
252
+ await manifestInput.fill(manifestUrl);
253
+ const installBtn = dialog
254
+ .locator('button[data-action="installPackage"], button:has-text("Install"), button.bright')
255
+ .filter({ visible: true })
256
+ .last();
257
+ await installBtn.evaluate((el) => el.click());
258
+ // Use the progress bar / notification wait logic
259
+ await page
260
+ .waitForFunction(() => {
261
+ const progress = document.querySelector(".notification.info, .progress-bar, .loading");
262
+ return !progress;
263
+ })
264
+ .catch(() => null);
265
+ // Close the dialog if it's still open
266
+ const closeBtn = dialog.locator('button[data-action="close"], .header-button.close');
267
+ if (await closeBtn.isVisible()) {
268
+ await closeBtn.click();
269
+ }
270
+ }
271
+ /**
272
+ * Waits for the Foundry VTT game object to be fully initialized and ready.
273
+ * @param page The Playwright Page object.
274
+ */
275
+ export async function waitForReady(page) {
276
+ console.log("[waitForReady] Waiting for game to be ready...");
277
+ await page.waitForFunction(() => window.game?.ready, { timeout: 60000 });
278
+ }
279
+ /**
280
+ * Executes a function after ensuring a log has been emitted via FP_VERIFY.
281
+ * @param page The Playwright Page object.
282
+ * @param key The log key to wait for.
283
+ * @param predicate A function to test the log data.
284
+ * @param extraData Optional extra data to pass to the predicate.
285
+ */
286
+ export async function verifyResult(page, key, predicate, extraData, options = {}) {
287
+ const { timeout = 15000 } = options;
288
+ console.log(`[verifyResult] Waiting for log "${key}" matching predicate...`);
289
+ // We must stringify the predicate to pass it into evaluate
290
+ const predicateStr = predicate.toString();
291
+ await page
292
+ .waitForFunction(({ key, predicateStr, extraData }) => {
293
+ try {
294
+ const predicate = new Function(`return ${predicateStr}`)();
295
+ const logs = window.FP_VERIFY?.logs[key] || [];
296
+ return logs.some((l) => predicate(l, extraData));
297
+ }
298
+ catch {
299
+ return false;
300
+ }
301
+ }, { key, predicateStr, extraData }, { timeout })
302
+ .catch((err) => {
303
+ console.error(`[verifyResult] Timeout waiting for log "${key}".`);
304
+ throw err;
305
+ });
306
+ }
307
+ /**
308
+ * Waits for a specific actor flag to be set to a value.
309
+ */
310
+ export async function waitForActorFlag(page, actorName, flag, value) {
311
+ await verifyResult(page, "actor-update", (data) => {
312
+ return data.name === actorName && data.delta.flags?.["fake-module"]?.[flag] === value;
313
+ });
314
+ }
315
+ /**
316
+ * Waits for specific actor data to be updated.
317
+ */
318
+ export async function waitForActorData(page, actorName, path, value) {
319
+ await verifyResult(page, "actor-update", (data) => {
320
+ const current = data.delta.system?.[path];
321
+ return data.name === actorName && current === value;
322
+ });
323
+ }
324
+ /**
325
+ * Waits for a game setting to be set.
326
+ */
327
+ export async function waitForSetting(page, module, key, value) {
328
+ // Note: Settings updates are usually logged or checked directly
329
+ await page.waitForFunction(({ module, key, value }) => {
330
+ return window.game.settings.get(module, key) === value;
331
+ }, { module, key, value });
332
+ }
333
+ /**
334
+ * Clears the FP_VERIFY log registry.
335
+ */
336
+ export async function clearFPVerify(page) {
337
+ await page.evaluate(() => {
338
+ if (window.FP_VERIFY_RESET)
339
+ window.FP_VERIFY_RESET();
340
+ });
341
+ }
342
+ /**
343
+ * Handles the Foundry VTT reload dialog.
344
+ */
345
+ export async function handleReload(page) {
346
+ const dialog = page
347
+ .locator("dialog, foundry-app, .window-app")
348
+ .filter({ hasText: /Reload/i })
349
+ .last();
350
+ await expect(dialog).toBeVisible();
351
+ await dialog.locator('button:has-text("Yes")').first().click();
352
+ await page.waitForLoadState("networkidle");
353
+ await waitForReady(page);
354
+ }
355
+ /**
356
+ * Fills a field in a visible dialog.
357
+ */
358
+ export async function fillDialogField(page, label, value) {
359
+ const dialog = page.locator("dialog, foundry-app, .window-app").filter({ visible: true }).last();
360
+ const input = dialog.locator(`input[name="${label}"], input[placeholder*="${label}" i]`).first();
361
+ await input.fill(value);
362
+ }
363
+ /**
364
+ * Performs the full module activation flow for a list of modules.
365
+ */
366
+ export async function handleModuleActivationFlow(page, moduleIds) {
367
+ const { foundrySetup } = await import("./auth.js");
368
+ await foundrySetup(page, { moduleId: moduleIds, createWorld: false, deleteIfExists: false });
369
+ }
370
+ /**
371
+ * Simulates a drop from a compendium onto a target.
372
+ */
373
+ export async function dropCompendiumItem(page, targetSelector, pack, itemId) {
374
+ const data = {
375
+ type: "Item",
376
+ uuid: `Compendium.${pack}.Item.${itemId}`,
377
+ };
378
+ await simulateFoundryDrop(page, targetSelector, data);
379
+ }
380
+ /**
381
+ * Simulates a Foundry VTT drag-and-drop event.
382
+ */
383
+ export async function simulateFoundryDrop(page, targetSelector, data) {
384
+ console.log(`[simulateFoundryDrop] Dropping ${data.type} onto ${targetSelector}...`);
385
+ await page.evaluate(({ selector, data }) => {
386
+ const selectors = selector.split(",").map((s) => s.trim());
387
+ let el = null;
388
+ for (const sel of selectors) {
389
+ const cleanSel = sel.replace(/:has-text\([^)]*\)/g, "");
390
+ try {
391
+ const matches = document.querySelectorAll(cleanSel);
392
+ if (sel.includes(":has-text")) {
393
+ const textMatch = sel.match(/:has-text\("([^"]*)"\)/);
394
+ const searchText = textMatch ? textMatch[1] : "";
395
+ el = Array.from(matches).find((m) => m.textContent?.includes(searchText));
396
+ }
397
+ else {
398
+ el = matches[0];
399
+ }
400
+ }
401
+ catch {
402
+ // Ignore selector errors
403
+ }
404
+ if (el)
405
+ break;
406
+ }
407
+ if (!el)
408
+ throw new Error(`Target ${selector} not found.`);
409
+ const dataTransfer = new DataTransfer();
410
+ dataTransfer.setData("text/plain", JSON.stringify(data));
411
+ el.dispatchEvent(new DragEvent("dragover", { dataTransfer, bubbles: true, cancelable: true }));
412
+ el.dispatchEvent(new DragEvent("drop", { dataTransfer, bubbles: true, cancelable: true }));
413
+ }, { selector: targetSelector, data });
414
+ }
@@ -0,0 +1,9 @@
1
+ export * from "./fixtures.js";
2
+ export { disableTour, switchTab, openSystemInstallDialog, openModuleInstallDialog, installSystemFromManifest, installModuleFromManifest, waitForActorFlag, waitForActorData, waitForSetting, clearFPVerify, simulateFoundryDrop, verifyResult, waitForReady, handleReload, fillDialogField, handleModuleActivationFlow, dropCompendiumItem, useFoundry, } from "./helpers.js";
3
+ export * from "./types/index.js";
4
+ export * from "./auth.js";
5
+ export * from "./state.js";
6
+ export * from "./systems/index.js";
7
+ export * from "./ui/index.js";
8
+ export * from "./docker.js";
9
+ export * from "./canvas.js";
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export * from "./fixtures.js";
2
+ export { disableTour, switchTab, openSystemInstallDialog, openModuleInstallDialog, installSystemFromManifest, installModuleFromManifest, waitForActorFlag, waitForActorData, waitForSetting, clearFPVerify, simulateFoundryDrop, verifyResult, waitForReady, handleReload, fillDialogField, handleModuleActivationFlow, dropCompendiumItem, useFoundry, } from "./helpers.js";
3
+ export * from "./types/index.js";
4
+ export * from "./auth.js";
5
+ export * from "./state.js";
6
+ export * from "./systems/index.js";
7
+ export * from "./ui/index.js";
8
+ export * from "./docker.js";
9
+ export * from "./canvas.js";
@@ -0,0 +1,97 @@
1
+ import { FoundryPage } from "../types/index.js";
2
+ /**
3
+ * Interface for version-specific Foundry VTT setup logic.
4
+ */
5
+ export interface SetupAdapter {
6
+ /** The major Foundry VTT version this adapter is for (e.g., 13, 14). */
7
+ version: number;
8
+ /**
9
+ * Switches between tabs on the setup screen.
10
+ * @param page The Foundry VTT Page object.
11
+ * @param tabName The logical name of the tab (e.g., "Worlds", "Systems").
12
+ */
13
+ switchTab(page: FoundryPage, tabName: string): Promise<void>;
14
+ /**
15
+ * Handles the End User License Agreement screen if it appears.
16
+ * @param page The Foundry VTT Page object.
17
+ */
18
+ handleEULA(page: FoundryPage): Promise<void>;
19
+ /**
20
+ * Handles the License Key Activation screen if it appears.
21
+ * @param page The Foundry VTT Page object.
22
+ * @param licenseKey The license key to activate (optional).
23
+ */
24
+ handleLicenseActivation(page: FoundryPage, licenseKey?: string): Promise<void>;
25
+ /**
26
+ * Installs a game system from the manifest list.
27
+ * @param page The Foundry VTT Page object.
28
+ * @param systemId The ID of the system to install.
29
+ * @param systemLabel The human-readable label of the system.
30
+ */
31
+ installSystem(page: FoundryPage, systemId: string, systemLabel: string): Promise<void>;
32
+ /**
33
+ * Installs one or more add-on modules from the manifest list.
34
+ * @param page The Foundry VTT Page object.
35
+ * @param moduleIds The ID(s) of the module(s) to install.
36
+ */
37
+ installModules(page: FoundryPage, moduleIds: string[]): Promise<void>;
38
+ /**
39
+ * Installs a game system from a direct manifest URL.
40
+ * @param page The Foundry VTT Page object.
41
+ * @param manifestUrl The URL to the system.json manifest.
42
+ */
43
+ installSystemFromManifest(page: FoundryPage, manifestUrl: string): Promise<void>;
44
+ /**
45
+ * Installs a module from a direct manifest URL.
46
+ * @param page The Foundry VTT Page object.
47
+ * @param manifestUrl The URL to the module.json manifest.
48
+ */
49
+ installModuleFromManifest(page: FoundryPage, manifestUrl: string): Promise<void>;
50
+ /**
51
+ * Opens the system installation dialog.
52
+ * @param page The Foundry VTT Page object.
53
+ */
54
+ openSystemInstallDialog(page: FoundryPage): Promise<any>;
55
+ /**
56
+ * Opens the module installation dialog.
57
+ * @param page The Foundry VTT Page object.
58
+ */
59
+ openModuleInstallDialog(page: FoundryPage): Promise<any>;
60
+ /**
61
+ * Creates a new game world.
62
+ * @param page The Foundry VTT Page object.
63
+ * @param worldId The ID for the new world.
64
+ * @param systemLabel The human-readable label of the system to use.
65
+ * @param systemId The unique ID of the game system to use.
66
+ */
67
+ createWorld(page: FoundryPage, worldId: string, systemLabel: string, systemId: string): Promise<void>;
68
+ /**
69
+ * Deletes a game world if it exists.
70
+ * @param page The Foundry VTT Page object.
71
+ * @param worldId The ID of the world to delete.
72
+ */
73
+ deleteWorldIfExists(page: FoundryPage, worldId: string): Promise<void>;
74
+ }
75
+ /**
76
+ * Interface for version-specific logic within the Foundry VTT game environment.
77
+ */
78
+ export interface GameAdapter {
79
+ /** The major Foundry VTT version this adapter is for. */
80
+ version: number;
81
+ createDocument(page: FoundryPage, documentName: string, data: any, options: any): Promise<any>;
82
+ updateDocument(page: FoundryPage, uuid: string, delta: any): Promise<any>;
83
+ deleteDocuments(page: FoundryPage, documentName: string, ids: string[], options: any): Promise<void>;
84
+ getDocuments(page: FoundryPage, collection: string, query: any): Promise<any[]>;
85
+ }
86
+ /**
87
+ * Base implementation of GameAdapter with shared logic for most versions.
88
+ */
89
+ export declare abstract class BaseGameAdapter implements GameAdapter {
90
+ protected page?: FoundryPage | undefined;
91
+ abstract version: number;
92
+ constructor(page?: FoundryPage | undefined);
93
+ createDocument(page: FoundryPage, documentName: string, data: any, options: any): Promise<any>;
94
+ updateDocument(page: FoundryPage, uuid: string, delta: any): Promise<any>;
95
+ deleteDocuments(page: FoundryPage, documentName: string, ids: string[], options: any): Promise<void>;
96
+ getDocuments(page: FoundryPage, collection: string, query: Record<string, any>): Promise<any[]>;
97
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Base implementation of GameAdapter with shared logic for most versions.
3
+ */
4
+ export class BaseGameAdapter {
5
+ page;
6
+ constructor(page) {
7
+ this.page = page;
8
+ }
9
+ async createDocument(page, documentName, data, options) {
10
+ return page.evaluate(async ({ documentName, data, options }) => {
11
+ const collectionName = (documentName.toLowerCase() + "s");
12
+ const collection = window.game[collectionName];
13
+ const cls = collection?.documentClass || window[documentName];
14
+ if (!cls)
15
+ throw new Error(`Document class ${documentName} not found.`);
16
+ return await cls.create(data, options);
17
+ }, { documentName, data, options });
18
+ }
19
+ async updateDocument(page, uuid, delta) {
20
+ return page.evaluate(async ({ uuid, delta }) => {
21
+ const doc = window.fromUuidSync ? window.fromUuidSync(uuid) : null;
22
+ if (doc)
23
+ return await doc.update(delta);
24
+ for (const collection of Object.values(window.game.collections || {})) {
25
+ const match = collection.getName ? collection.getName(uuid) : null;
26
+ if (match)
27
+ return await match.update(delta);
28
+ }
29
+ throw new Error(`Document ${uuid} not found.`);
30
+ }, { uuid, delta });
31
+ }
32
+ async deleteDocuments(page, documentName, ids, options) {
33
+ await page.evaluate(async ({ documentName, ids, options }) => {
34
+ const cls = window[documentName];
35
+ if (!cls)
36
+ throw new Error(`Document class ${documentName} not found.`);
37
+ await cls.deleteDocuments(ids, options);
38
+ }, { documentName, ids, options });
39
+ }
40
+ async getDocuments(page, collection, query) {
41
+ return page.evaluate(({ collection, query }) => {
42
+ const coll = window.game[collection];
43
+ if (!coll)
44
+ return [];
45
+ // Simple query matching
46
+ return coll
47
+ .filter((d) => {
48
+ return Object.entries(query).every(([k, v]) => d[k] === v);
49
+ })
50
+ .map((d) => d.toObject?.() || d.toJSON());
51
+ }, { collection, query });
52
+ }
53
+ }
@@ -0,0 +1,14 @@
1
+ import { FoundryPage } from "../types/index.js";
2
+ import { SetupAdapter, GameAdapter } from "./base.js";
3
+ /**
4
+ * Detects the Foundry VTT version and returns the appropriate setup adapter.
5
+ * Prioritizes explicit version input from parameters or environment variables.
6
+ */
7
+ export declare function getSetupAdapter(page: FoundryPage, versionOverride?: string | number): Promise<SetupAdapter>;
8
+ /**
9
+ * Detects the Foundry VTT version and returns the appropriate game adapter.
10
+ */
11
+ export declare function getGameAdapter(page: FoundryPage, versionOverride?: string | number): Promise<GameAdapter>;
12
+ export * from "./base.js";
13
+ export * from "./v13.js";
14
+ export * from "./v14.js";