@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.
- package/README.md +79 -0
- package/dist/auth.d.ts +18 -0
- package/dist/auth.js +287 -0
- package/dist/canvas.d.ts +47 -0
- package/dist/canvas.js +105 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +83 -0
- package/dist/cli/init.d.ts +5 -0
- package/dist/cli/init.js +129 -0
- package/dist/deprecations.d.ts +24 -0
- package/dist/deprecations.js +59 -0
- package/dist/docker.d.ts +37 -0
- package/dist/docker.js +140 -0
- package/dist/fixtures.d.ts +29 -0
- package/dist/fixtures.js +112 -0
- package/dist/helpers.d.ts +100 -0
- package/dist/helpers.js +414 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/setup/base.d.ts +97 -0
- package/dist/setup/base.js +53 -0
- package/dist/setup/index.d.ts +14 -0
- package/dist/setup/index.js +126 -0
- package/dist/setup/v13.d.ts +28 -0
- package/dist/setup/v13.js +308 -0
- package/dist/setup/v14.d.ts +31 -0
- package/dist/setup/v14.js +421 -0
- package/dist/state.d.ts +139 -0
- package/dist/state.js +321 -0
- package/dist/systems/base.d.ts +48 -0
- package/dist/systems/base.js +57 -0
- package/dist/systems/dnd5e.d.ts +27 -0
- package/dist/systems/dnd5e.js +30 -0
- package/dist/systems/index.d.ts +13 -0
- package/dist/systems/index.js +20 -0
- package/dist/systems/pf2e.d.ts +25 -0
- package/dist/systems/pf2e.js +62 -0
- package/dist/types/index.d.ts +18 -0
- package/dist/types/index.js +11 -0
- package/dist/ui/base.d.ts +35 -0
- package/dist/ui/base.js +43 -0
- package/dist/ui/dnd5e.d.ts +8 -0
- package/dist/ui/dnd5e.js +10 -0
- package/dist/ui/index.d.ts +45 -0
- package/dist/ui/index.js +72 -0
- package/dist/ui/tidy5e.d.ts +11 -0
- package/dist/ui/tidy5e.js +30 -0
- package/package.json +67 -0
package/dist/helpers.js
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
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";
|
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";
|