@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
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { expect } from "@playwright/test";
|
|
2
|
+
import { BaseGameAdapter } from "./base.js";
|
|
3
|
+
import { installSystemFromManifest as helperInstallSystemFromManifest, installModuleFromManifest as helperInstallModuleFromManifest, } from "../helpers.js";
|
|
4
|
+
/**
|
|
5
|
+
* Setup adapter for Foundry VTT Version 14.
|
|
6
|
+
*/
|
|
7
|
+
export class V14SetupAdapter {
|
|
8
|
+
version = 14;
|
|
9
|
+
constructor(page) {
|
|
10
|
+
if (page?.deprecationTracker) {
|
|
11
|
+
page.deprecationTracker.registerIgnore(["namespaced under foundry"]);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
async switchTab(page, tabName) {
|
|
15
|
+
const tabMap = {
|
|
16
|
+
Worlds: "worlds",
|
|
17
|
+
"Game Worlds": "worlds",
|
|
18
|
+
Systems: "systems",
|
|
19
|
+
"Game Systems": "systems",
|
|
20
|
+
Modules: "modules",
|
|
21
|
+
"Add-on Modules": "modules",
|
|
22
|
+
Configuration: "config",
|
|
23
|
+
"Update Software": "update",
|
|
24
|
+
};
|
|
25
|
+
const dataTab = tabMap[tabName] || tabName.toLowerCase();
|
|
26
|
+
console.log(`[V14SetupAdapter] Switching to setup tab: ${tabName} (${dataTab})`);
|
|
27
|
+
const alreadyActive = await page.evaluate((dt) => {
|
|
28
|
+
const section = document.querySelector(`[data-application-part="${dt}"]`);
|
|
29
|
+
return (section?.classList.contains("active") &&
|
|
30
|
+
section.getClientRects().length > 0);
|
|
31
|
+
}, dataTab);
|
|
32
|
+
if (alreadyActive) {
|
|
33
|
+
console.log(`[V14SetupAdapter] Tab ${tabName} is already active and visible.`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const tabLocator = page
|
|
37
|
+
.locator(`[data-tab="${dataTab}"], [data-action="tab"][data-tab="${dataTab}"], .tabs .item:has-text("${tabName}"), h2:has-text("${tabName}"), [data-application-part="header"] button:has-text("${tabName}")`)
|
|
38
|
+
.filter({ visible: true })
|
|
39
|
+
.first();
|
|
40
|
+
await expect(tabLocator).toBeVisible({ timeout: 20000 });
|
|
41
|
+
// Wait for it to not be disabled (RFC 0008: Worlds tab depends on System installation)
|
|
42
|
+
await page
|
|
43
|
+
.waitForFunction((dt) => {
|
|
44
|
+
const tab = document.querySelector(`[data-tab="${dt}"], [data-action="tab"][data-tab="${dt}"]`);
|
|
45
|
+
return !tab?.classList.contains("disabled");
|
|
46
|
+
}, dataTab, { timeout: 30000 })
|
|
47
|
+
.catch(() => {
|
|
48
|
+
console.warn(`[V14SetupAdapter] Tab ${tabName} is still marked as disabled.`);
|
|
49
|
+
});
|
|
50
|
+
console.log(`[V14SetupAdapter] Clicking tab: ${tabName}`);
|
|
51
|
+
await tabLocator.evaluate((el) => el.click());
|
|
52
|
+
// Wait for the specific part to be active
|
|
53
|
+
await page.waitForFunction((dt) => {
|
|
54
|
+
const section = document.querySelector(`[data-application-part="${dt}"]`);
|
|
55
|
+
return section?.classList.contains("active");
|
|
56
|
+
}, dataTab, { timeout: 15000 });
|
|
57
|
+
await page.waitForTimeout(500);
|
|
58
|
+
}
|
|
59
|
+
async handleEULA(page) {
|
|
60
|
+
console.log("[V14SetupAdapter] Checking for Analytics/EULA...");
|
|
61
|
+
// 0. Handle License Key Activation
|
|
62
|
+
await this.handleLicenseActivation(page, process.env.FOUNDRY_LICENSE_KEY);
|
|
63
|
+
// 1. Handle Analytics/Usage Data dialog FIRST as it often overlaps
|
|
64
|
+
// We must be careful not to match the EULA itself here.
|
|
65
|
+
const analyticsDialog = page
|
|
66
|
+
.locator("dialog, foundry-app, .application, .window-app")
|
|
67
|
+
.filter({
|
|
68
|
+
hasText: /Usage Data|Sharing/i,
|
|
69
|
+
})
|
|
70
|
+
.filter({
|
|
71
|
+
hasNot: page.locator('#license-title, h1:has-text("End User License Agreement")'),
|
|
72
|
+
});
|
|
73
|
+
if ((await analyticsDialog.count()) > 0) {
|
|
74
|
+
console.log("[V14SetupAdapter] Analytics dialog detected. Declining...");
|
|
75
|
+
const declineBtn = analyticsDialog
|
|
76
|
+
.locator('button[data-action="no"], button:has-text("Decline"), button:has-text("No")')
|
|
77
|
+
.filter({ visible: true })
|
|
78
|
+
.first();
|
|
79
|
+
if (await declineBtn.isVisible()) {
|
|
80
|
+
await declineBtn.evaluate((el) => el.click());
|
|
81
|
+
await page.waitForTimeout(1000);
|
|
82
|
+
}
|
|
83
|
+
// Force remove if still present
|
|
84
|
+
await page.evaluate(() => {
|
|
85
|
+
document.querySelectorAll("dialog, foundry-app, .application").forEach((el) => {
|
|
86
|
+
const text = el.textContent?.toLowerCase() || "";
|
|
87
|
+
if ((text.includes("usage data") || text.includes("sharing")) &&
|
|
88
|
+
!text.includes("license")) {
|
|
89
|
+
if (el.tagName.toLowerCase() === "dialog")
|
|
90
|
+
el.close?.();
|
|
91
|
+
el.remove();
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
// 2. Traditional EULA
|
|
97
|
+
const eulaHeading = page.locator("#license-title");
|
|
98
|
+
if (page.url().includes("/license") || (await eulaHeading.count()) > 0) {
|
|
99
|
+
console.log("[V14SetupAdapter] EULA screen detected. Processing agreement...");
|
|
100
|
+
// Ensure the footer is visible
|
|
101
|
+
const acknowledgeHeading = page.getByRole("heading", { name: "Acknowledge Agreement" });
|
|
102
|
+
await acknowledgeHeading.waitFor({ state: "visible", timeout: 10000 }).catch(() => {
|
|
103
|
+
console.warn("[V14SetupAdapter] 'Acknowledge Agreement' heading not found, continuing anyway...");
|
|
104
|
+
});
|
|
105
|
+
// Ensure we are at the bottom
|
|
106
|
+
await page.evaluate(() => {
|
|
107
|
+
const eulaContainer = document.querySelector(".scrollable, .license-text, #eula-content, section.license, .window-content");
|
|
108
|
+
if (eulaContainer)
|
|
109
|
+
eulaContainer.scrollTop = eulaContainer.scrollHeight;
|
|
110
|
+
});
|
|
111
|
+
let checkbox = page.getByLabel("I agree to these terms").first();
|
|
112
|
+
if ((await checkbox.count()) === 0) {
|
|
113
|
+
console.log("[V14SetupAdapter] getByLabel failed, falling back to broader locators...");
|
|
114
|
+
checkbox = page
|
|
115
|
+
.locator('#eula-agree, #license-agree, input[type="checkbox"][name="agree"], input[type="checkbox"][name="license-agree"]')
|
|
116
|
+
.first();
|
|
117
|
+
}
|
|
118
|
+
if ((await checkbox.count()) > 0) {
|
|
119
|
+
console.log("[V14SetupAdapter] EULA checkbox found.");
|
|
120
|
+
if (!(await checkbox.isChecked())) {
|
|
121
|
+
// Using evaluate to check because standard click can be intercepted
|
|
122
|
+
await checkbox.evaluate((el) => (el.checked = true));
|
|
123
|
+
await checkbox.dispatchEvent("change");
|
|
124
|
+
console.log("[V14SetupAdapter] EULA checkbox checked.");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
throw new Error("[V14SetupAdapter] EULA agreement checkbox NOT found. Cannot proceed with setup.");
|
|
129
|
+
}
|
|
130
|
+
let agreementBtn = page.getByRole("button", { name: "Agree" }).first();
|
|
131
|
+
if ((await agreementBtn.count()) === 0) {
|
|
132
|
+
agreementBtn = page
|
|
133
|
+
.locator('button[data-action="agree"], button[type="submit"], button#sign')
|
|
134
|
+
.first();
|
|
135
|
+
}
|
|
136
|
+
if ((await agreementBtn.count()) > 0 && (await agreementBtn.isVisible())) {
|
|
137
|
+
console.log("[V14SetupAdapter] EULA agreement button found. Clicking...");
|
|
138
|
+
await agreementBtn.evaluate((el) => el.click());
|
|
139
|
+
await page.waitForLoadState("networkidle");
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
throw new Error("[V14SetupAdapter] EULA agreement button NOT found or NOT visible. Cannot proceed with setup.");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async handleLicenseActivation(page, licenseKey) {
|
|
147
|
+
const licenseHeading = page.getByRole("heading", { name: "License Key Activation" });
|
|
148
|
+
if ((await licenseHeading.count()) > 0 && (await licenseHeading.isVisible())) {
|
|
149
|
+
console.log("[V14SetupAdapter] License Key Activation screen detected.");
|
|
150
|
+
if (!licenseKey) {
|
|
151
|
+
throw new Error("[V14SetupAdapter] Foundry VTT requires a license key but FOUNDRY_LICENSE_KEY is not set.");
|
|
152
|
+
}
|
|
153
|
+
console.log("[V14SetupAdapter] Entering license key...");
|
|
154
|
+
const keyInput = page.getByPlaceholder("XXXX-XXXX-XXXX-XXXX-XXXX-XXXX");
|
|
155
|
+
await keyInput.fill(licenseKey);
|
|
156
|
+
const submitBtn = page.getByRole("button", { name: "Submit Key" });
|
|
157
|
+
await submitBtn.evaluate((el) => el.click());
|
|
158
|
+
await page.waitForLoadState("networkidle");
|
|
159
|
+
console.log("[V14SetupAdapter] License key submitted.");
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async installSystem(page, systemId, _systemLabel) {
|
|
163
|
+
console.log(`[V14SetupAdapter] Installing system: ${systemId}`);
|
|
164
|
+
await this.switchTab(page, "Systems");
|
|
165
|
+
// Search locally first
|
|
166
|
+
const setupFilter = page
|
|
167
|
+
.locator('#system-filter, #setup-packages-systems input[type="search"]')
|
|
168
|
+
.first();
|
|
169
|
+
await setupFilter.fill(systemId);
|
|
170
|
+
await page.keyboard.press("Enter");
|
|
171
|
+
await page.waitForTimeout(1000);
|
|
172
|
+
const localPackage = page.locator(`#systems-list [data-package-id="${systemId}"]`).first();
|
|
173
|
+
if (await localPackage.isVisible()) {
|
|
174
|
+
console.log(`[V14SetupAdapter] System ${systemId} is already installed.`);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
// Click "Search Installable Packages" or the main "Install System" button
|
|
178
|
+
const searchRemoteBtn = page
|
|
179
|
+
.locator('button.search-packages[data-action="installPackage"]')
|
|
180
|
+
.filter({ visible: true })
|
|
181
|
+
.first();
|
|
182
|
+
let installDialog;
|
|
183
|
+
if (await searchRemoteBtn.isVisible()) {
|
|
184
|
+
console.log("[V14SetupAdapter] Clicking 'Search Installable Packages' button...");
|
|
185
|
+
await searchRemoteBtn.evaluate((el) => el.click());
|
|
186
|
+
installDialog = await this.findInstallerDialog(page);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
installDialog = await this.openSystemInstallDialog(page);
|
|
190
|
+
}
|
|
191
|
+
await expect(installDialog).toBeVisible({ timeout: 30000 });
|
|
192
|
+
// Ensure we are on Systems tab in installer
|
|
193
|
+
await this.ensureInstallerTab(page, installDialog, "system");
|
|
194
|
+
// Use manifest fallback for dnd5e to ensure tests pass
|
|
195
|
+
if (systemId === "dnd5e") {
|
|
196
|
+
console.log("[V14SetupAdapter] Using manifest installation for dnd5e.");
|
|
197
|
+
const manifestUrl = "https://raw.githubusercontent.com/foundryvtt/dnd5e/master/system.json";
|
|
198
|
+
const manifestInput = installDialog
|
|
199
|
+
.locator('input#install-package-manifestUrl, input[name="manifestURL"], input[placeholder*="URL" i]')
|
|
200
|
+
.first();
|
|
201
|
+
await manifestInput.fill(manifestUrl);
|
|
202
|
+
const installBtn = installDialog
|
|
203
|
+
.locator('button[data-action="installUrl"], button[data-action="installPackage"], button:has-text("Install")')
|
|
204
|
+
.last();
|
|
205
|
+
await installBtn.evaluate((el) => el.click());
|
|
206
|
+
await this.waitForInstallation(page, installDialog, `[data-package-id="${systemId}"]`, "Systems");
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
// Standard remote search
|
|
210
|
+
const filterBox = installDialog
|
|
211
|
+
.locator('input#install-package-search-filter, input[type="search"]')
|
|
212
|
+
.first();
|
|
213
|
+
await filterBox.fill(systemId);
|
|
214
|
+
await page.keyboard.press("Enter");
|
|
215
|
+
await page.waitForTimeout(4000);
|
|
216
|
+
const packageRow = installDialog
|
|
217
|
+
.locator(`.package[data-package-id="${systemId}"], [data-package-id="${systemId}"], li:has-text("${systemId}"), .package:has-text("${systemId}")`)
|
|
218
|
+
.filter({ visible: true })
|
|
219
|
+
.first();
|
|
220
|
+
await expect(packageRow).toBeVisible({ timeout: 30000 });
|
|
221
|
+
const installButton = packageRow
|
|
222
|
+
.locator('button[data-action="installPackage"], button:has-text("Install")')
|
|
223
|
+
.filter({ visible: true })
|
|
224
|
+
.first();
|
|
225
|
+
await installButton.evaluate((el) => el.click());
|
|
226
|
+
await this.waitForInstallation(page, installDialog, `[data-package-id="${systemId}"]`, "Systems");
|
|
227
|
+
}
|
|
228
|
+
async installModules(page, moduleIds) {
|
|
229
|
+
console.log(`[V14SetupAdapter] Installing modules: ${moduleIds.join(", ")}`);
|
|
230
|
+
for (const modId of moduleIds) {
|
|
231
|
+
await this.switchTab(page, "Modules");
|
|
232
|
+
const setupFilter = page
|
|
233
|
+
.locator('#module-filter, #setup-packages-modules input[type="search"]')
|
|
234
|
+
.first();
|
|
235
|
+
await setupFilter.fill(modId);
|
|
236
|
+
await page.keyboard.press("Enter");
|
|
237
|
+
await page.waitForTimeout(1000);
|
|
238
|
+
const moduleBox = page
|
|
239
|
+
.locator(`.package[data-package-id="${modId}"], [data-package-id="${modId}"]`)
|
|
240
|
+
.filter({ visible: true })
|
|
241
|
+
.first();
|
|
242
|
+
if (await moduleBox.isVisible())
|
|
243
|
+
continue;
|
|
244
|
+
const searchRemoteBtn = page
|
|
245
|
+
.locator('button.search-packages[data-action="installPackage"]')
|
|
246
|
+
.filter({ visible: true })
|
|
247
|
+
.first();
|
|
248
|
+
if (await searchRemoteBtn.isVisible()) {
|
|
249
|
+
await searchRemoteBtn.evaluate((el) => el.click());
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
await this.openModuleInstallDialog(page);
|
|
253
|
+
}
|
|
254
|
+
const installDialog = await this.findInstallerDialog(page);
|
|
255
|
+
await this.ensureInstallerTab(page, installDialog, "module");
|
|
256
|
+
const filterBox = installDialog
|
|
257
|
+
.locator('input#install-package-search-filter, input[type="search"]')
|
|
258
|
+
.first();
|
|
259
|
+
await filterBox.fill(modId);
|
|
260
|
+
await page.keyboard.press("Enter");
|
|
261
|
+
await page.waitForTimeout(4000);
|
|
262
|
+
const packageRow = installDialog
|
|
263
|
+
.locator(`.package[data-package-id="${modId}"], [data-package-id="${modId}"], li:has-text("${modId}"), .package:has-text("${modId}")`)
|
|
264
|
+
.filter({ visible: true })
|
|
265
|
+
.first();
|
|
266
|
+
if (await packageRow.isVisible()) {
|
|
267
|
+
const installButton = packageRow
|
|
268
|
+
.locator('button[data-action="installPackage"], button:has-text("Install")')
|
|
269
|
+
.filter({ visible: true })
|
|
270
|
+
.first();
|
|
271
|
+
await installButton.evaluate((el) => el.click());
|
|
272
|
+
await this.waitForInstallation(page, installDialog, `[data-package-id="${modId}"]`, "Modules");
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
async installSystemFromManifest(page, manifestUrl) {
|
|
277
|
+
await helperInstallSystemFromManifest(page, manifestUrl);
|
|
278
|
+
}
|
|
279
|
+
async installModuleFromManifest(page, manifestUrl) {
|
|
280
|
+
await helperInstallModuleFromManifest(page, manifestUrl);
|
|
281
|
+
}
|
|
282
|
+
async findInstallerDialog(page) {
|
|
283
|
+
// Find any dialog that is NOT the main setup shell
|
|
284
|
+
return page
|
|
285
|
+
.locator("#install-package, div.category-browser, .application:not(#setup-packages)")
|
|
286
|
+
.filter({ hasText: /Install|Package|Installer/i })
|
|
287
|
+
.filter({ visible: true })
|
|
288
|
+
.last();
|
|
289
|
+
}
|
|
290
|
+
async ensureInstallerTab(page, dialog, type) {
|
|
291
|
+
await page
|
|
292
|
+
.evaluate(({ type }) => {
|
|
293
|
+
const dialog = document.querySelector("#install-package, .category-browser");
|
|
294
|
+
const root = dialog || document;
|
|
295
|
+
const tabs = Array.from(root.querySelectorAll(`.tabs .item, .tabs button, [data-action="tab"]`));
|
|
296
|
+
const target = tabs.find((t) => t.dataset.tab?.includes(type) || t.textContent?.toLowerCase().includes(type));
|
|
297
|
+
if (target && !target.classList.contains("active"))
|
|
298
|
+
target.click();
|
|
299
|
+
}, { type })
|
|
300
|
+
.catch(() => null);
|
|
301
|
+
await page.waitForTimeout(1000);
|
|
302
|
+
}
|
|
303
|
+
async openSystemInstallDialog(page) {
|
|
304
|
+
await this.switchTab(page, "Systems");
|
|
305
|
+
const installBtn = page
|
|
306
|
+
.locator('[data-application-part="systems"] button[data-action="installPackage"], button:has-text("Install System")')
|
|
307
|
+
.filter({ visible: true })
|
|
308
|
+
.first();
|
|
309
|
+
await installBtn.evaluate((el) => el.click());
|
|
310
|
+
await page.waitForTimeout(2000);
|
|
311
|
+
return this.findInstallerDialog(page);
|
|
312
|
+
}
|
|
313
|
+
async openModuleInstallDialog(page) {
|
|
314
|
+
await this.switchTab(page, "Modules");
|
|
315
|
+
const installBtn = page
|
|
316
|
+
.locator('[data-application-part="modules"] button[data-action="installPackage"], button:has-text("Install Module")')
|
|
317
|
+
.filter({ visible: true })
|
|
318
|
+
.first();
|
|
319
|
+
await installBtn.evaluate((el) => el.click());
|
|
320
|
+
await page.waitForTimeout(2000);
|
|
321
|
+
return this.findInstallerDialog(page);
|
|
322
|
+
}
|
|
323
|
+
async createWorld(page, worldId, systemLabel, systemId) {
|
|
324
|
+
console.log(`[V14SetupAdapter] Creating world: ${worldId}`);
|
|
325
|
+
await this.switchTab(page, "Worlds");
|
|
326
|
+
const createBtn = page
|
|
327
|
+
.locator('button[data-action="worldCreate"], button:has-text("Create World")')
|
|
328
|
+
.filter({ visible: true })
|
|
329
|
+
.first();
|
|
330
|
+
await createBtn.evaluate((el) => el.click());
|
|
331
|
+
await page.waitForTimeout(2000);
|
|
332
|
+
if (page.url().includes("/create")) {
|
|
333
|
+
console.log("[V14SetupAdapter] On world creation screen. Filling form...");
|
|
334
|
+
await page.waitForLoadState("networkidle");
|
|
335
|
+
const configSection = page.locator('section[data-application-part="config"]');
|
|
336
|
+
await configSection.locator('input[name="title"]').fill(worldId);
|
|
337
|
+
const worldIdInput = configSection.locator('input[name="world-id"], input[name="id"]');
|
|
338
|
+
if ((await worldIdInput.count()) > 0)
|
|
339
|
+
await worldIdInput.fill(worldId);
|
|
340
|
+
console.log(`[V14SetupAdapter] Selecting system: ${systemId}`);
|
|
341
|
+
const systemGallery = page.locator('section.systems[data-application-part="systems"]');
|
|
342
|
+
await systemGallery.locator('input[type="search"]').fill(systemId);
|
|
343
|
+
const systemItem = systemGallery
|
|
344
|
+
.locator(`li.package.system[data-package-id="${systemId}"], li:has-text("${systemId}")`)
|
|
345
|
+
.filter({ visible: true })
|
|
346
|
+
.first();
|
|
347
|
+
await systemItem.evaluate((el) => el.click());
|
|
348
|
+
const submitBtn = page
|
|
349
|
+
.locator('button[type="submit"].bright, button:has-text("Create World")')
|
|
350
|
+
.first();
|
|
351
|
+
await submitBtn.evaluate((el) => el.click());
|
|
352
|
+
console.log("[V14SetupAdapter] Waiting for players or setup redirection...");
|
|
353
|
+
await page.waitForURL((u) => u.pathname.includes("/players") || u.pathname.includes("/setup"), { timeout: 60000 });
|
|
354
|
+
if (page.url().includes("/players")) {
|
|
355
|
+
console.log("[V14SetupAdapter] Redirection to /players detected. Clicking Save Configuration...");
|
|
356
|
+
const playersSubmitBtn = page
|
|
357
|
+
.locator('button[type="submit"].bright, button:has-text("Save Configuration")')
|
|
358
|
+
.first();
|
|
359
|
+
await playersSubmitBtn.evaluate((el) => el.click());
|
|
360
|
+
await page.waitForLoadState("networkidle");
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
async deleteWorldIfExists(page, worldId) {
|
|
365
|
+
console.log(`[V14SetupAdapter] Deleting world if exists: ${worldId}`);
|
|
366
|
+
await this.switchTab(page, "Worlds");
|
|
367
|
+
const worldBox = page
|
|
368
|
+
.locator(`.package[data-package-id="${worldId}"], [data-package-id="${worldId}"]`)
|
|
369
|
+
.first();
|
|
370
|
+
if (await worldBox.isVisible()) {
|
|
371
|
+
const stopButton = worldBox.locator('[data-action="worldStop"]');
|
|
372
|
+
if (await stopButton.isVisible()) {
|
|
373
|
+
await stopButton.evaluate((el) => el.click());
|
|
374
|
+
await expect(worldBox.locator('[data-action="worldLaunch"]')).toBeVisible({
|
|
375
|
+
timeout: 10000,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
await worldBox.click({ button: "right" });
|
|
379
|
+
const deleteOption = page.locator("li.context-item").filter({ hasText: /Delete World/i });
|
|
380
|
+
await deleteOption.evaluate((el) => el.click());
|
|
381
|
+
const dialog = page
|
|
382
|
+
.locator("dialog, .application")
|
|
383
|
+
.filter({ hasText: /Delete World/i })
|
|
384
|
+
.last();
|
|
385
|
+
await expect(dialog).toBeVisible();
|
|
386
|
+
const confirmCode = await dialog.locator(".reference, .confirm-code").first().innerText();
|
|
387
|
+
const input = dialog
|
|
388
|
+
.locator('input#delete-confirm, input[name="confirm"], input[name="world-id"]')
|
|
389
|
+
.first();
|
|
390
|
+
await input.fill(confirmCode);
|
|
391
|
+
await dialog
|
|
392
|
+
.locator('button[data-action="yes"], button:has-text("Yes"), button.bright')
|
|
393
|
+
.first()
|
|
394
|
+
.evaluate((el) => el.click());
|
|
395
|
+
await expect(worldBox).toBeHidden({ timeout: 15000 });
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
async waitForInstallation(page, dialog, verificationSelector, tabName) {
|
|
399
|
+
await page.waitForTimeout(5000);
|
|
400
|
+
const closeBtn = dialog
|
|
401
|
+
.locator('button[data-action="close"], .header-control.close, .header-button.close')
|
|
402
|
+
.filter({ visible: true })
|
|
403
|
+
.first();
|
|
404
|
+
if (await closeBtn.isVisible())
|
|
405
|
+
await closeBtn.evaluate((el) => el.click());
|
|
406
|
+
await this.switchTab(page, "Worlds");
|
|
407
|
+
await this.switchTab(page, tabName);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Game adapter for Foundry VTT Version 14.
|
|
412
|
+
*/
|
|
413
|
+
export class V14GameAdapter extends BaseGameAdapter {
|
|
414
|
+
version = 14;
|
|
415
|
+
constructor(page) {
|
|
416
|
+
super(page);
|
|
417
|
+
if (page?.deprecationTracker) {
|
|
418
|
+
page.deprecationTracker.registerIgnore(["namespaced under foundry"]);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
package/dist/state.d.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { FoundryPage, UserRole } from "./types/index.js";
|
|
2
|
+
import { DeprecationTracker } from "./deprecations.js";
|
|
3
|
+
/**
|
|
4
|
+
* Provides methods for direct manipulation of the Foundry VTT state.
|
|
5
|
+
*/
|
|
6
|
+
export declare class FoundryState {
|
|
7
|
+
private page;
|
|
8
|
+
private systemId;
|
|
9
|
+
private deprecationTracker?;
|
|
10
|
+
private adapter;
|
|
11
|
+
constructor(page: FoundryPage, systemId?: string, deprecationTracker?: DeprecationTracker | undefined);
|
|
12
|
+
/**
|
|
13
|
+
* Sets the system adapter to use.
|
|
14
|
+
*/
|
|
15
|
+
setSystem(systemId: string): void;
|
|
16
|
+
/**
|
|
17
|
+
* Aggressively removes properties known to trigger deprecation warnings on access.
|
|
18
|
+
* This is used before returning data from page.evaluate.
|
|
19
|
+
*/
|
|
20
|
+
private static get SanitizerScript();
|
|
21
|
+
/**
|
|
22
|
+
* Creates a new Foundry VTT document.
|
|
23
|
+
* @param documentName The type of document (e.g., "Actor", "Item").
|
|
24
|
+
* @param data The document data.
|
|
25
|
+
*/
|
|
26
|
+
createDocument(documentName: string, data: any): Promise<any>;
|
|
27
|
+
/**
|
|
28
|
+
* Updates an existing document in Foundry VTT.
|
|
29
|
+
* @param documentName The name of the document type.
|
|
30
|
+
* @param id The ID of the document to update.
|
|
31
|
+
* @param delta The data to update.
|
|
32
|
+
*/
|
|
33
|
+
updateDocument(documentName: string, id: string, delta: any): Promise<any>;
|
|
34
|
+
/**
|
|
35
|
+
* Deletes a document in Foundry VTT.
|
|
36
|
+
* @param documentName The name of the document type.
|
|
37
|
+
* @param id The ID of the document to delete.
|
|
38
|
+
*/
|
|
39
|
+
deleteDocument(documentName: string, id: string): Promise<any>;
|
|
40
|
+
/**
|
|
41
|
+
* Gets a document by its ID.
|
|
42
|
+
* @param documentName The name of the document type.
|
|
43
|
+
* @param id The ID of the document.
|
|
44
|
+
*/
|
|
45
|
+
getDocument(documentName: string, id: string): Promise<any>;
|
|
46
|
+
/**
|
|
47
|
+
* Gets a document by its name.
|
|
48
|
+
* @param documentName The name of the document type.
|
|
49
|
+
* @param name The name of the document.
|
|
50
|
+
*/
|
|
51
|
+
getDocumentByName(documentName: string, name: string): Promise<any>;
|
|
52
|
+
/**
|
|
53
|
+
* Creates a new User.
|
|
54
|
+
*/
|
|
55
|
+
createUser(name: string, role?: UserRole, password?: string): Promise<any>;
|
|
56
|
+
/**
|
|
57
|
+
* Sets a user's role.
|
|
58
|
+
*/
|
|
59
|
+
setUserRole(userId: string, role: UserRole): Promise<any>;
|
|
60
|
+
/**
|
|
61
|
+
* Configures a specific permission for a user role.
|
|
62
|
+
*/
|
|
63
|
+
setRolePermission(permission: string, role: UserRole, allowed: boolean): Promise<any>;
|
|
64
|
+
/**
|
|
65
|
+
* Grants currency to an actor.
|
|
66
|
+
* @param actorName The name of the actor.
|
|
67
|
+
* @param amount The amount of currency to grant.
|
|
68
|
+
* @param currency The type of currency (e.g., "gp", "sp").
|
|
69
|
+
*/
|
|
70
|
+
grantCurrency(actorName: string, amount: number, currency?: string): Promise<any>;
|
|
71
|
+
/**
|
|
72
|
+
* Gets the verification parameters for a currency update.
|
|
73
|
+
*/
|
|
74
|
+
getCurrencyVerifyParams(actorName: string, amount: number, currency?: string): {
|
|
75
|
+
key: string;
|
|
76
|
+
predicate: (data: any, extra?: any) => boolean;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Sets an actor's HP.
|
|
80
|
+
* @param actorName The name of the actor.
|
|
81
|
+
* @param value The new HP value.
|
|
82
|
+
* @param max The new max HP value (optional).
|
|
83
|
+
*/
|
|
84
|
+
setActorHP(actorName: string, value: number, max?: number): Promise<any>;
|
|
85
|
+
/**
|
|
86
|
+
* Rolls a specific roll for an actor.
|
|
87
|
+
* @param actorName The name of the actor.
|
|
88
|
+
* @param formula The roll formula (e.g., "1d20 + 5").
|
|
89
|
+
* @param label A label for the roll.
|
|
90
|
+
*/
|
|
91
|
+
roll(actorName: string, formula: string, label?: string): Promise<any>;
|
|
92
|
+
/**
|
|
93
|
+
* Executes a macro by name.
|
|
94
|
+
* @param name The name of the macro.
|
|
95
|
+
* @param args Arguments to pass to the macro.
|
|
96
|
+
*/
|
|
97
|
+
executeMacro(name: string, ...args: any[]): Promise<any>;
|
|
98
|
+
/**
|
|
99
|
+
* Manually triggers a Foundry VTT hook.
|
|
100
|
+
* @param hookName The name of the hook (e.g., "renderActorSheet").
|
|
101
|
+
* @param args Arguments to pass to the hook.
|
|
102
|
+
*/
|
|
103
|
+
triggerHook(hookName: string, ...args: any[]): Promise<any>;
|
|
104
|
+
/**
|
|
105
|
+
* Emits a socket event via the Foundry VTT socket.
|
|
106
|
+
* @param eventName The name of the event.
|
|
107
|
+
* @param data The data to emit.
|
|
108
|
+
*/
|
|
109
|
+
emitSocket(eventName: string, data: any): Promise<void>;
|
|
110
|
+
/**
|
|
111
|
+
* Waits for a specific Foundry VTT hook to be called.
|
|
112
|
+
* @param hookName The name of the hook.
|
|
113
|
+
* @param timeout The timeout in milliseconds.
|
|
114
|
+
* @returns The first argument passed to the hook.
|
|
115
|
+
*/
|
|
116
|
+
waitForHook(hookName: string, timeout?: number): Promise<unknown>;
|
|
117
|
+
/**
|
|
118
|
+
* Waits for a specific socket event to be received.
|
|
119
|
+
* @param eventName The name of the event.
|
|
120
|
+
* @param timeout The timeout in milliseconds.
|
|
121
|
+
*/
|
|
122
|
+
waitForSocket(eventName: string, timeout?: number): Promise<number>;
|
|
123
|
+
/**
|
|
124
|
+
* Assigns an actor to a user.
|
|
125
|
+
*/
|
|
126
|
+
assignActorToUser(userId: string, actorId: string): Promise<any>;
|
|
127
|
+
/**
|
|
128
|
+
* Updates an existing user.
|
|
129
|
+
*/
|
|
130
|
+
updateUser(userId: string, delta: any): Promise<any>;
|
|
131
|
+
/**
|
|
132
|
+
* Creates a test actor.
|
|
133
|
+
*/
|
|
134
|
+
createTestActor(name?: string): Promise<any>;
|
|
135
|
+
/**
|
|
136
|
+
* Sets or updates a Foundry VTT setting.
|
|
137
|
+
*/
|
|
138
|
+
setSetting(module: string, key: string, value: any): Promise<any>;
|
|
139
|
+
}
|