@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,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
|
+
}
|