@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,126 @@
1
+ import { V13SetupAdapter, V13GameAdapter } from "./v13.js";
2
+ import { V14SetupAdapter, V14GameAdapter } from "./v14.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 async function getSetupAdapter(page, versionOverride) {
8
+ // 1. Prioritize explicit input
9
+ const explicitVersion = versionOverride || process.env.FOUNDRY_VERSION;
10
+ if (explicitVersion) {
11
+ const v = String(explicitVersion);
12
+ if (v.startsWith("14"))
13
+ return new V14SetupAdapter(page);
14
+ if (v.startsWith("13"))
15
+ return new V13SetupAdapter(page);
16
+ console.warn(`[getSetupAdapter] Explicit version "${v}" provided but not explicitly supported. Falling back to detection.`);
17
+ }
18
+ console.log("[getSetupAdapter] Detecting Foundry version...");
19
+ // Wait for definitive detection
20
+ const detectedVersion = await page
21
+ .waitForFunction(() => {
22
+ // 1. Check for Version String (Most reliable if available)
23
+ const v = window.game?.version ||
24
+ window.game?.release?.generation ||
25
+ window.foundry?.utils?.vttVersion;
26
+ if (v) {
27
+ const vs = String(v);
28
+ if (vs.startsWith("14"))
29
+ return 14;
30
+ if (vs.startsWith("13"))
31
+ return 13;
32
+ }
33
+ // 2. Check for V14 definitive markers (ApplicationV2 shell)
34
+ const isV14 = window.foundry?.applications?.api?.ApplicationV2 !== undefined ||
35
+ document.querySelector("foundry-app") !== null ||
36
+ document.body.classList.contains("v14");
37
+ if (isV14)
38
+ return 14;
39
+ // 3. Check for V13 definitive markers
40
+ // V13 uses traditional body classes and does NOT have foundry-app
41
+ const isV13 = (document.body.classList.contains("setup") ||
42
+ document.body.classList.contains("join") ||
43
+ document.body.classList.contains("game")) &&
44
+ document.querySelector("foundry-app") === null;
45
+ if (isV13)
46
+ return 13;
47
+ // 4. Script-based fallback (V12- used foundry.js, V13+ uses foundry.mjs)
48
+ const scripts = Array.from(document.querySelectorAll("script")).map((s) => s.src);
49
+ if (scripts.some((s) => s.includes("foundry.mjs"))) {
50
+ // If it's foundry.mjs but didn't match V14 markers yet, it might be V13
51
+ // or V14 hasn't fully loaded its shell. We wait.
52
+ return null;
53
+ }
54
+ if (scripts.some((s) => s.includes("scripts/foundry.js")))
55
+ return 13; // V12/V13 early? Actually V13 is mjs.
56
+ return null; // Not detectable yet
57
+ }, {}, { timeout: 30000 })
58
+ .then((h) => h.jsonValue())
59
+ .catch(async () => {
60
+ const diag = await page.evaluate(() => {
61
+ return {
62
+ url: window.location.href,
63
+ html: document.body.innerHTML.substring(0, 500),
64
+ foundry: !!window.foundry,
65
+ vttVersion: window.foundry?.utils?.vttVersion,
66
+ scripts: Array.from(document.querySelectorAll("script")).map((s) => s.src),
67
+ };
68
+ });
69
+ console.warn(`[getSetupAdapter] Detection timed out at ${diag.url}. Diag: ${JSON.stringify(diag)}`);
70
+ // Fallback logic in catch block
71
+ if (diag.vttVersion) {
72
+ if (String(diag.vttVersion).startsWith("14"))
73
+ return 14;
74
+ if (String(diag.vttVersion).startsWith("13"))
75
+ return 13;
76
+ }
77
+ if (diag.url.includes("/players") || diag.url.includes("/create"))
78
+ return 14;
79
+ if (diag.scripts.some((s) => s.includes("foundry.mjs"))) {
80
+ // If we are here, we timed out. If it has foundry.mjs and NO foundry-app, it's likely 13.
81
+ const hasFoundryApp = await page.evaluate(() => document.querySelector("foundry-app") !== null);
82
+ return hasFoundryApp ? 14 : 13;
83
+ }
84
+ return 13; // Default to 13
85
+ });
86
+ if (detectedVersion === 14)
87
+ return new V14SetupAdapter(page);
88
+ return new V13SetupAdapter(page);
89
+ }
90
+ /**
91
+ * Detects the Foundry VTT version and returns the appropriate game adapter.
92
+ */
93
+ export async function getGameAdapter(page, versionOverride) {
94
+ // 1. Prioritize explicit input
95
+ const explicitVersion = versionOverride || process.env.FOUNDRY_VERSION;
96
+ if (explicitVersion) {
97
+ const v = String(explicitVersion);
98
+ if (v.startsWith("14"))
99
+ return new V14GameAdapter(page);
100
+ if (v.startsWith("13"))
101
+ return new V13GameAdapter(page);
102
+ }
103
+ const version = await page
104
+ .waitForFunction(() => {
105
+ const v = window.game?.version ||
106
+ window.game?.release?.generation ||
107
+ window.foundry?.utils?.vttVersion;
108
+ if (v) {
109
+ if (String(v).startsWith("14"))
110
+ return 14;
111
+ if (String(v).startsWith("13"))
112
+ return 13;
113
+ }
114
+ if (window.foundry?.applications?.api?.ApplicationV2 !== undefined)
115
+ return 14;
116
+ return null;
117
+ }, {}, { timeout: 30000 })
118
+ .then((h) => h.jsonValue())
119
+ .catch(() => 13);
120
+ if (version === 14)
121
+ return new V14GameAdapter(page);
122
+ return new V13GameAdapter(page);
123
+ }
124
+ export * from "./base.js";
125
+ export * from "./v13.js";
126
+ export * from "./v14.js";
@@ -0,0 +1,28 @@
1
+ import { SetupAdapter, BaseGameAdapter } from "./base.js";
2
+ import { FoundryPage } from "../types/index.js";
3
+ /**
4
+ * Setup adapter for Foundry VTT Version 13.
5
+ */
6
+ export declare class V13SetupAdapter implements SetupAdapter {
7
+ version: number;
8
+ constructor(page?: FoundryPage);
9
+ switchTab(page: FoundryPage, tabName: string): Promise<void>;
10
+ handleEULA(page: FoundryPage): Promise<void>;
11
+ handleLicenseActivation(page: FoundryPage, licenseKey?: string): Promise<void>;
12
+ installSystem(page: FoundryPage, systemId: string, _systemLabel: string): Promise<void>;
13
+ installModules(page: FoundryPage, moduleIds: string[]): Promise<void>;
14
+ installSystemFromManifest(page: FoundryPage, manifestUrl: string): Promise<void>;
15
+ installModuleFromManifest(page: FoundryPage, manifestUrl: string): Promise<void>;
16
+ openSystemInstallDialog(page: FoundryPage): Promise<any>;
17
+ openModuleInstallDialog(page: FoundryPage): Promise<any>;
18
+ createWorld(page: FoundryPage, worldId: string, systemLabel: string, systemId: string): Promise<void>;
19
+ deleteWorldIfExists(page: FoundryPage, worldId: string): Promise<void>;
20
+ private waitForInstallation;
21
+ }
22
+ /**
23
+ * Game adapter for Foundry VTT Version 13.
24
+ */
25
+ export declare class V13GameAdapter extends BaseGameAdapter {
26
+ version: number;
27
+ constructor(page?: FoundryPage);
28
+ }
@@ -0,0 +1,308 @@
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 13.
6
+ */
7
+ export class V13SetupAdapter {
8
+ version = 13;
9
+ constructor(page) {
10
+ if (page?.deprecationTracker) {
11
+ // Add version-specific ignores if needed
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(`[V13SetupAdapter] Switching to setup tab: ${tabName} (${dataTab})`);
27
+ // Ensure navigation is visible
28
+ const nav = page.locator("nav, .navigation, #setup-packages-modules").first();
29
+ await nav.waitFor({ state: "attached", timeout: 20000 });
30
+ const tabLocator = page
31
+ .locator("nav h2, .navigation h2, h2, #setup-packages-modules h2")
32
+ .filter({ hasText: new RegExp(tabName, "i") })
33
+ .first();
34
+ console.log(`[V13SetupAdapter] Waiting for tab locator visibility for "${tabName}"...`);
35
+ await expect(tabLocator).toBeVisible({ timeout: 20000 });
36
+ const tabText = await tabLocator.innerText();
37
+ console.log(`[V13SetupAdapter] Found tab: "${tabText}". Clicking...`);
38
+ // Click and wait for active class
39
+ await tabLocator.evaluate((el) => el.click());
40
+ await page.waitForFunction(({ name, dt }) => {
41
+ const h2 = Array.from(document.querySelectorAll("nav h2, .navigation h2, h2")).find((el) => el.textContent?.toLowerCase().includes(name.toLowerCase()) ||
42
+ el.textContent?.toLowerCase().includes(dt.toLowerCase()));
43
+ return h2?.classList.contains("active");
44
+ }, { name: tabName, dt: dataTab }, { timeout: 10000 });
45
+ // Wait for content section visibility
46
+ await page.waitForFunction((dt) => {
47
+ const section = document.querySelector(`#setup-packages-${dt}`);
48
+ return section && !section.getAttribute("style")?.includes("display: none");
49
+ }, dataTab, { timeout: 10000 });
50
+ console.log(`[V13SetupAdapter] Tab ${tabName} is now active.`);
51
+ }
52
+ async handleEULA(page) {
53
+ console.log("[V13SetupAdapter] Handling EULA...");
54
+ // 0. Handle License Key Activation
55
+ await this.handleLicenseActivation(page, process.env.FOUNDRY_LICENSE_KEY);
56
+ await page.evaluate(() => {
57
+ const eulaContainer = document.querySelector(".scrollable, .license-text, #eula-content");
58
+ if (eulaContainer)
59
+ eulaContainer.scrollTop = eulaContainer.scrollHeight;
60
+ });
61
+ const checkbox = page.locator('input[type="checkbox"], [name="agree"]').first();
62
+ if ((await checkbox.count()) > 0) {
63
+ if (!(await checkbox.isChecked())) {
64
+ await checkbox.click({ force: true });
65
+ }
66
+ }
67
+ else {
68
+ throw new Error("[V13SetupAdapter] EULA agreement checkbox NOT found. Cannot proceed with setup.");
69
+ }
70
+ const eulaButton = page.locator("button").filter({ hasText: /agree|sign|accept/i });
71
+ if ((await eulaButton.count()) > 0) {
72
+ await eulaButton.first().evaluate((el) => el.click());
73
+ try {
74
+ await page.waitForURL((u) => !u.pathname.includes("/license"), { timeout: 20000 });
75
+ }
76
+ catch {
77
+ throw new Error("[V13SetupAdapter] Failed to navigate away from EULA screen after clicking Agree.");
78
+ }
79
+ }
80
+ else {
81
+ throw new Error("[V13SetupAdapter] Stuck on /license but no agreement button found.");
82
+ }
83
+ }
84
+ async handleLicenseActivation(page, licenseKey) {
85
+ const licenseHeading = page.getByRole("heading", { name: "License Key Activation" });
86
+ if ((await licenseHeading.count()) > 0 && (await licenseHeading.isVisible())) {
87
+ console.log("[V13SetupAdapter] License Key Activation screen detected.");
88
+ if (!licenseKey) {
89
+ throw new Error("[V13SetupAdapter] Foundry VTT requires a license key but FOUNDRY_LICENSE_KEY is not set.");
90
+ }
91
+ console.log("[V13SetupAdapter] Entering license key...");
92
+ const keyInput = page.getByPlaceholder("XXXX-XXXX-XXXX-XXXX-XXXX-XXXX");
93
+ await keyInput.fill(licenseKey);
94
+ const submitBtn = page.getByRole("button", { name: "Submit Key" });
95
+ await submitBtn.click();
96
+ await page.waitForLoadState("networkidle");
97
+ console.log("[V13SetupAdapter] License key submitted.");
98
+ }
99
+ }
100
+ async installSystem(page, systemId, _systemLabel) {
101
+ console.log(`[V13SetupAdapter] Installing system: ${systemId}`);
102
+ await this.switchTab(page, "Systems");
103
+ // Check if already installed
104
+ const localPackage = page
105
+ .locator(`#setup-packages-systems [data-package-id="${systemId}"]`)
106
+ .first();
107
+ if (await localPackage.isVisible()) {
108
+ console.log(`[V13SetupAdapter] System ${systemId} is already installed.`);
109
+ return;
110
+ }
111
+ const installDialog = await this.openSystemInstallDialog(page);
112
+ const filterBox = installDialog.getByRole("searchbox", { name: "Filter" });
113
+ await filterBox.click({ clickCount: 3 });
114
+ await page.keyboard.press("Backspace");
115
+ await page.keyboard.type(systemId, { delay: 50 });
116
+ await page.keyboard.press("Enter");
117
+ await page.waitForTimeout(5000);
118
+ const packageRow = installDialog.locator(`[data-package-id="${systemId}"]`).first();
119
+ await expect(packageRow).toBeVisible({ timeout: 15000 });
120
+ const installButton = packageRow
121
+ .locator('button[data-action="installPackage"], button:has-text("Install")')
122
+ .first();
123
+ if ((await installButton.count()) > 0) {
124
+ await installButton.evaluate((el) => el.click());
125
+ await this.waitForInstallation(page, installDialog, `[data-package-id="${systemId}"]`, "Systems");
126
+ }
127
+ else {
128
+ throw new Error(`Failed to find Install button for system: ${systemId}`);
129
+ }
130
+ }
131
+ async installModules(page, moduleIds) {
132
+ console.log(`[V13SetupAdapter] Installing modules: ${moduleIds.join(", ")}`);
133
+ await this.switchTab(page, "Modules");
134
+ for (const modId of moduleIds) {
135
+ const moduleBox = page
136
+ .locator(`[data-package-id="${modId}"], [data-module-id="${modId}"]`)
137
+ .first();
138
+ if ((await moduleBox.count()) === 0 || (await moduleBox.isHidden())) {
139
+ const installDialog = await this.openModuleInstallDialog(page);
140
+ const filterBox = installDialog.getByRole("searchbox", { name: "Filter" });
141
+ await filterBox.click({ clickCount: 3 });
142
+ await page.keyboard.press("Backspace");
143
+ await page.keyboard.type(modId, { delay: 50 });
144
+ await page.keyboard.press("Enter");
145
+ await page.waitForTimeout(5000);
146
+ const packageRow = installDialog.locator(`[data-package-id="${modId}"]`).first();
147
+ await expect(packageRow).toBeVisible({ timeout: 15000 });
148
+ const installButton = packageRow
149
+ .locator('button[data-action="installPackage"], button:has-text("Install")')
150
+ .first();
151
+ if ((await installButton.count()) > 0) {
152
+ await installButton.evaluate((el) => el.click());
153
+ await this.waitForInstallation(page, installDialog, `[data-package-id="${modId}"]`, "Modules");
154
+ }
155
+ else {
156
+ throw new Error(`Failed to find Install button for module: ${modId}`);
157
+ }
158
+ }
159
+ }
160
+ }
161
+ async installSystemFromManifest(page, manifestUrl) {
162
+ await helperInstallSystemFromManifest(page, manifestUrl);
163
+ }
164
+ async installModuleFromManifest(page, manifestUrl) {
165
+ await helperInstallModuleFromManifest(page, manifestUrl);
166
+ }
167
+ async openSystemInstallDialog(page) {
168
+ console.log("[V13SetupAdapter] Opening System Install Dialog...");
169
+ await this.switchTab(page, "Systems");
170
+ const installBtn = page
171
+ .locator("button:visible")
172
+ .filter({ hasText: /Install System/i })
173
+ .first();
174
+ await expect(installBtn).toBeVisible({ timeout: 10000 });
175
+ await installBtn.evaluate((el) => el.click());
176
+ const dialog = page
177
+ .locator("dialog, #install-package, .application.category-browser, foundry-app")
178
+ .filter({ hasText: /Install System|Install Package/i })
179
+ .last();
180
+ await expect(dialog).toBeVisible({ timeout: 30000 });
181
+ return dialog;
182
+ }
183
+ async openModuleInstallDialog(page) {
184
+ console.log("[V13SetupAdapter] Opening Module Install Dialog...");
185
+ await this.switchTab(page, "Modules");
186
+ const installBtn = page
187
+ .locator("button:visible")
188
+ .filter({ hasText: /Install Module/i })
189
+ .first();
190
+ await expect(installBtn).toBeVisible({ timeout: 10000 });
191
+ await installBtn.evaluate((el) => el.click());
192
+ const dialog = page
193
+ .locator("dialog, #install-package, .application.category-browser, foundry-app")
194
+ .filter({ hasText: /Install Module|Install Package|Install System/i })
195
+ .last();
196
+ await expect(dialog).toBeVisible({ timeout: 30000 });
197
+ return dialog;
198
+ }
199
+ async createWorld(page, worldId, systemLabel, systemId) {
200
+ console.log(`[V13SetupAdapter] Creating world: ${worldId}`);
201
+ await this.switchTab(page, "Worlds");
202
+ const createBtn = page
203
+ .locator("button")
204
+ .filter({ hasText: /Create World/i })
205
+ .first();
206
+ console.log("[V13SetupAdapter] Clicking Create World button...");
207
+ await createBtn.evaluate((el) => el.click());
208
+ // Target the specific world-config form
209
+ const createDialog = page
210
+ .locator("form#world-config, .application, .window-app")
211
+ .filter({ hasText: /World Configuration|Create World/i })
212
+ .last();
213
+ await createDialog.waitFor({ state: "visible", timeout: 20000 });
214
+ console.log("[V13SetupAdapter] World creation dialog is visible.");
215
+ const titleInput = createDialog.locator('input[name="title"], input[name*="title" i], input[placeholder*="Title" i]');
216
+ await titleInput.first().fill(worldId);
217
+ const pathInput = createDialog.locator('input[name="id"], input[name="name"], input[name*="path" i], input[placeholder*="Path" i]');
218
+ if (await pathInput.first().isVisible()) {
219
+ await pathInput.first().fill(worldId);
220
+ }
221
+ const systemSelect = createDialog.locator('select[name="system"], select[name*="system" i], select.system-select');
222
+ // Attempt to select by label, then fallback to value (id)
223
+ try {
224
+ await systemSelect.first().selectOption({ label: systemLabel }, { timeout: 5000 });
225
+ }
226
+ catch {
227
+ console.warn(`[V13SetupAdapter] Failed to select system by label "${systemLabel}". Trying ID "${systemId}"...`);
228
+ // We use systemId passed from createWorld call
229
+ await systemSelect.first().selectOption({ value: systemId });
230
+ }
231
+ const submitBtn = createDialog
232
+ .locator('button[type="submit"], button')
233
+ .filter({ hasText: /Create World|Create/i })
234
+ .first();
235
+ await submitBtn.evaluate((el) => el.click());
236
+ // Wait for the specific form to be removed
237
+ await expect(page.locator("form#world-config")).toBeHidden({ timeout: 20000 });
238
+ }
239
+ async deleteWorldIfExists(page, worldId) {
240
+ console.log(`[V13SetupAdapter] Deleting world if exists: ${worldId}`);
241
+ await this.switchTab(page, "Worlds");
242
+ const worldBox = page.locator(`[data-package-id="${worldId}"]`).first();
243
+ if ((await worldBox.count()) === 1 && (await worldBox.isVisible())) {
244
+ const stopButton = worldBox.locator('[data-action="worldStop"]');
245
+ if ((await stopButton.count()) === 1 && (await stopButton.isVisible())) {
246
+ await stopButton.click();
247
+ await expect(worldBox.locator('[data-action="worldLaunch"]')).toBeVisible({
248
+ timeout: 10000,
249
+ });
250
+ }
251
+ await worldBox.dispatchEvent("contextmenu");
252
+ const deleteOption = page.locator("li.context-item, .context-item").filter({
253
+ hasText: /Delete World/i,
254
+ });
255
+ await deleteOption.click();
256
+ const dialog = page
257
+ .locator("dialog, .application, .window-app")
258
+ .filter({ hasText: new RegExp(`Delete World: ${worldId}`, "i") })
259
+ .last();
260
+ await expect(dialog).toBeVisible();
261
+ const confirmCode = await dialog.locator(".reference").innerText();
262
+ await dialog.getByRole("textbox").fill(confirmCode);
263
+ await dialog.getByRole("button", { name: "Yes" }).click();
264
+ await expect(worldBox).toBeHidden({ timeout: 15000 });
265
+ }
266
+ }
267
+ async waitForInstallation(page, dialog, verificationSelector, tabName) {
268
+ await page.waitForTimeout(5000);
269
+ const progressNotification = page.locator(".notification", {
270
+ hasText: /Downloading|Installing/i,
271
+ });
272
+ try {
273
+ await progressNotification.waitFor({ state: "visible", timeout: 15000 });
274
+ await progressNotification.waitFor({ state: "hidden", timeout: 300000 });
275
+ }
276
+ catch {
277
+ console.log("[V13SetupAdapter] Installation notification not seen or finished very quickly.");
278
+ }
279
+ const closeBtn = dialog.locator('button.header-button.control.close, [data-action="close"]');
280
+ if (await closeBtn.isVisible()) {
281
+ await closeBtn.click();
282
+ }
283
+ try {
284
+ await page
285
+ .locator(verificationSelector)
286
+ .first()
287
+ .waitFor({ state: "visible", timeout: 30000 });
288
+ }
289
+ catch {
290
+ console.log("[V13SetupAdapter] Package not immediately visible. Refreshing tab...");
291
+ await this.switchTab(page, "Worlds"); // Transition away and back
292
+ await this.switchTab(page, tabName);
293
+ await page
294
+ .locator(verificationSelector)
295
+ .first()
296
+ .waitFor({ state: "visible", timeout: 30000 });
297
+ }
298
+ }
299
+ }
300
+ /**
301
+ * Game adapter for Foundry VTT Version 13.
302
+ */
303
+ export class V13GameAdapter extends BaseGameAdapter {
304
+ version = 13;
305
+ constructor(page) {
306
+ super(page);
307
+ }
308
+ }
@@ -0,0 +1,31 @@
1
+ import { Locator } from "@playwright/test";
2
+ import { SetupAdapter, BaseGameAdapter } from "./base.js";
3
+ import { FoundryPage } from "../types/index.js";
4
+ /**
5
+ * Setup adapter for Foundry VTT Version 14.
6
+ */
7
+ export declare class V14SetupAdapter implements SetupAdapter {
8
+ version: number;
9
+ constructor(page?: FoundryPage);
10
+ switchTab(page: FoundryPage, tabName: string): Promise<void>;
11
+ handleEULA(page: FoundryPage): Promise<void>;
12
+ handleLicenseActivation(page: FoundryPage, licenseKey?: string): Promise<void>;
13
+ installSystem(page: FoundryPage, systemId: string, _systemLabel: string): Promise<void>;
14
+ installModules(page: FoundryPage, moduleIds: string[]): Promise<void>;
15
+ installSystemFromManifest(page: FoundryPage, manifestUrl: string): Promise<void>;
16
+ installModuleFromManifest(page: FoundryPage, manifestUrl: string): Promise<void>;
17
+ findInstallerDialog(page: FoundryPage): Promise<Locator>;
18
+ ensureInstallerTab(page: FoundryPage, dialog: Locator, type: string): Promise<void>;
19
+ openSystemInstallDialog(page: FoundryPage): Promise<Locator>;
20
+ openModuleInstallDialog(page: FoundryPage): Promise<Locator>;
21
+ createWorld(page: FoundryPage, worldId: string, systemLabel: string, systemId: string): Promise<void>;
22
+ deleteWorldIfExists(page: FoundryPage, worldId: string): Promise<void>;
23
+ private waitForInstallation;
24
+ }
25
+ /**
26
+ * Game adapter for Foundry VTT Version 14.
27
+ */
28
+ export declare class V14GameAdapter extends BaseGameAdapter {
29
+ version: number;
30
+ constructor(page?: FoundryPage);
31
+ }