Now manage to replace the Foundry native AV dock
CI / ci (push) Failing after 16s

This commit is contained in:
2026-05-24 09:39:53 +02:00
parent c4a375f4e3
commit 6d7a0b5fd7
94 changed files with 6027 additions and 20 deletions
@@ -0,0 +1,703 @@
# Blind Hunter — WebRTC Full AV Replacement Code Review
**Diff Source:** Commit range HEAD~1..HEAD (c4a375f)
**Review Target:** WebRTC full AV replacement implementation
**Date:** 2026-05-26
---
## INSTRUCTIONS
You are the **Blind Hunter** — a cynical, jaded code reviewer with zero patience for sloppy work. You expect to find problems. Be skeptical of everything. Look for what's missing, not just what's wrong. Use a precise, professional tone — no profanity or personal attacks.
**YOU RECEIVE ONLY THIS DIFF.** You have NO project context, NO spec, NO access to the codebase. Review ONLY what is shown in the diff below.
## DIFF TO REVIEW
```diff
diff --git a/module.js b/module.js
index 86d48ac..c5fe82f 100644
--- a/module.js
+++ b/module.js
@@ -57,15 +57,17 @@ let directorsBoardButtonAdded = false;
Hooks.once("init", () => {
console.log("[ScryingPool] init — module loading");
- // OQ-1 resolved (Story 1.2 spike): probe result is 'css-fallback' — see FoundryAdapter.js
+ // WebRTC mode setting — determines how the module handles AV integration
+ // Updated for FULL REPLACEMENT architecture (hiding Foundry's dock, showing our own)
game.settings.register("scrying-pool", "webrtcMode", {
scope: "world",
config: false,
type: String,
- default: "css-fallback",
+ default: "stream-access",
choices: {
- "track-disable": "Track Disable (bandwidth-saving)",
- "css-fallback": "CSS Fallback (cosmetic hiding)",
+ "stream-access": "Stream Access (full replacement - hide Foundry dock, show VVM dock with actual video)",
+ "track-disable": "Track Disable (bandwidth-saving) - DEPRECATED",
+ "css-fallback": "CSS Fallback (cosmetic hiding) - DEPRECATED",
"unsupported": "Unsupported (AV not available)",
},
});
@@ -175,14 +177,25 @@ Hooks.once("ready", () => {
// Hydrate StateStore from persisted world setting (AC-6, AC-7)
stateStore.init();
- // Probe WebRTC capability and set adapter.webrtc (AC-8)
+ // Probe WebRTC capability and set adapter.webrtc
+ // For FULL REPLACEMENT: we need stream access to create our own video tiles
const outcome = FoundryAdapter.probeCapability(game.webrtc);
- adapter.webrtc = outcome === 'track-disable'
- ? FoundryAdapter.buildWebRTCSurface(game.webrtc)
- : null;
adapter.settings.set(FoundryAdapter.SETTING_WEBRTC_MODE, outcome).catch(err => {
console.error('[ScryingPool] Failed to set webrtcMode setting:', err);
});
+
+ // Build WebRTC surface for stream access (full replacement mode)
+ if (outcome === 'stream-access') {
+ adapter.webrtc = FoundryAdapter.buildWebRTCSurface(game.webrtc);
+ console.log('[ScryingPool] WebRTC stream access available - full replacement mode enabled');
+ } else if (outcome === 'unsupported') {
+ adapter.webrtc = null;
+ console.log('[ScryingPool] WebRTC not available - AV features disabled');
+ } else {
+ // Legacy: track-disable or css-fallback (deprecated)
+ adapter.webrtc = FoundryAdapter.buildWebRTCSurface(game.webrtc);
+ console.warn('[ScryingPool] WebRTC mode is deprecated:', outcome, '- consider using stream-access');
+ }
// Wire core managers — construct both before setReady so handler can reference both
visibilityManager = new VisibilityManager(stateStore, adapter);
diff --git a/scripts/test-stream-access.mjs b/scripts/test-stream-access.mjs
new file mode 100644
index 0000000..8d91db7
--- /dev/null
+++ b/scripts/test-stream-access.mjs
@@ -0,0 +1,159 @@
+/**
+ * Test script to verify WebRTC stream access implementation
+ * This script tests the full AV replacement architecture
+ *
+ * Run with: node scripts/test-stream-access.mjs
+ *
+ * Tests:
+ * 1. FoundryAdapter.probeCapability() returns 'stream-access' when getMediaStreamForUser is available
+ * 2. FoundryAdapter.buildWebRTCSurface() creates proper WebRTC surface
+ * 3. ScryingPoolStrip.getData() includes hasStreamAccess flag
+ * 4. buildParticipantList() includes hasStreamAccess in participant objects
+ * 5. CSS includes rules to hide Foundry's AV dock
+ * 6. Template includes video container element
+ */
+
+import { FoundryAdapter } from '../src/foundry/FoundryAdapter.js';
+import { buildParticipantList } from '../src/ui/gm/ScryingPoolStrip.js';
+
+console.log('=== WebRTC Stream Access Implementation Tests ===\n');
+
+// Test 1: probeCapability returns 'stream-access' when getMediaStreamForUser is available
+console.log('Test 1: probeCapability with stream access');
+const mockWebRTCWithStream = {
+ client: {
+ getMediaStreamForUser: (userId) => new MediaStream(),
+ getConnectedUsers: () => ['user1', 'user2'],
+ },
+};
+const result1 = FoundryAdapter.probeCapability(mockWebRTCWithStream);
+console.log(` probeCapability result: ${result1}`);
+console.assert(result1 === 'stream-access', 'Should return stream-access');
+console.log(' ✓ PASSED\n');
+
+// Test 2: probeCapability returns 'unsupported' when getMediaStreamForUser is missing
+console.log('Test 2: probeCapability without stream access');
+const mockWebRTCWithoutStream = {
+ client: {},
+};
+const result2 = FoundryAdapter.probeCapability(mockWebRTCWithoutStream);
+console.log(` probeCapability result: ${result2}`);
+console.assert(result2 === 'unsupported', 'Should return unsupported');
+console.log(' ✓ PASSED\n');
+
+// Test 3: buildWebRTCSurface creates proper surface with all methods
+console.log('Test 3: buildWebRTCSurface creates complete WebRTC surface');
+const surface = FoundryAdapter.buildWebRTCSurface(mockWebRTCWithStream);
+console.log(' WebRTC surface methods:');
+console.log(' - getMediaStreamForUser:', typeof surface.getMediaStreamForUser === 'function' ? '✓' : '✗');
+console.log(' - getConnectedUsers:', typeof surface.getConnectedUsers === 'function' ? '✓' : '✗');
+console.log(' - getLevelsStreamForUser:', typeof surface.getLevelsStreamForUser === 'function' ? '✓' : '✗');
+console.log(' - isAudioEnabled:', typeof surface.isAudioEnabled === 'function' ? '✓' : '✗');
+console.log(' - isVideoEnabled:', typeof surface.isVideoEnabled === 'function' ? '✓' : '✗');
+console.log(' - toggleAudio:', typeof surface.toggleAudio === 'function' ? '✓' : '✗');
+console.log(' - toggleVideo:', typeof surface.toggleVideo === 'function' ? '✓' : '✗');
+console.log(' - toggleBroadcast:', typeof surface.toggleBroadcast === 'function' ? '✓' : '✗');
+console.log(' - setUserVideo:', typeof surface.setUserVideo === 'function' ? '✓' : '✗');
+console.log(' - disableTrack:', typeof surface.disableTrack === 'function' ? '✓' : '✗');
+console.log(' - enableTrack:', typeof surface.enableTrack === 'function' ? '✓' : '✗');
+
+const allMethodsPresent =
+ typeof surface.getMediaStreamForUser === 'function' &&
+ typeof surface.getConnectedUsers === 'function' &&
+ typeof surface.getLevelsStreamForUser === 'function' &&
+ typeof surface.isAudioEnabled === 'function' &&
+ typeof surface.isVideoEnabled === 'function' &&
+ typeof surface.toggleAudio === 'function' &&
+ typeof surface.toggleVideo === 'function' &&
+ typeof surface.toggleBroadcast === 'function' &&
+ typeof surface.setUserVideo === 'function' &&
+ typeof surface.disableTrack === 'function' &&
+ typeof surface.enableTrack === 'function';
+
+console.assert(allMethodsPresent, 'All WebRTC surface methods should be functions');
+console.log(' ✓ PASSED\n');
+
+// Test 4: buildParticipantList includes hasStreamAccess
+console.log('Test 4: buildParticipantList includes hasStreamAccess flag');
+const mockAdapter = {
+ users: {
+ get: (userId) => ({ name: userId, avatar: null }),
+ },
+};
+const mockStateStore = {
+ getState: (userId) => 'active',
+};
+const mockController = {
+ hasPendingOp: (userId) => false,
+};
+
+const participants = buildParticipantList(
+ ['user1', 'user2'],
+ mockStateStore,
+ mockController,
+ mockAdapter,
+ true // hasStreamAccess
+);
+
+console.log(` Participant count: ${participants.length}`);
+console.log(` First participant hasStreamAccess: ${participants[0].hasStreamAccess}`);
+console.assert(participants.length === 2, 'Should have 2 participants');
+console.assert(participants[0].hasStreamAccess === true, 'Participant should have hasStreamAccess flag');
+console.log(' ✓ PASSED\n');
+
+// Test 5: CSS includes rules to hide Foundry's AV dock
+console.log('Test 5: CSS includes AV dock hiding rules');
+import fs from 'fs';
+import path from 'path';
+
+const cssPath = path.join(process.cwd(), 'styles', 'scrying-pool.less');
+const cssContent = fs.readFileSync(cssPath, 'utf8');
+
+const hasAvHiding = cssContent.includes('#av') && cssContent.includes('display: none');
+const hasCameraViewHiding = cssContent.includes('.camera-view') && cssContent.includes('display: none');
+
+console.log(` Has #av hiding rule: ${hasAvHiding ? '✓' : '✗'}`);
+console.log(` Has .camera-view hiding rule: ${hasCameraViewHiding ? '✓' : '✗'}`);
+console.assert(hasAvHiding, 'CSS should include #av hiding rule');
+console.assert(hasCameraViewHiding, 'CSS should include .camera-view hiding rule');
+console.log(' ✓ PASSED\n');
+
+// Test 6: Template includes video container
+console.log('Test 6: Template includes video container element');
+const templatePath = path.join(process.cwd(), 'templates', 'roster-strip.hbs');
+const templateContent = fs.readFileSync(templatePath, 'utf8');
+
+const hasVideoContainer = templateContent.includes('sp-participant-video');
+const hasStreamAccessCheck = templateContent.includes('hasStreamAccess');
+
+console.log(` Has video container class: ${hasVideoContainer ? '✓' : '✗'}`);
+console.log(` Has stream access check: ${hasStreamAccessCheck ? '✓' : '✗'}`);
+console.assert(hasVideoContainer, 'Template should include video container');
+console.assert(hasStreamAccessCheck, 'Template should check for stream access');
+console.log(' ✓ PASSED\n');
+
+// Test 7: CSS includes video element styling
+console.log('Test 7: CSS includes video element styling');
+const rosterCssPath = path.join(process.cwd(), 'styles', 'components', '_roster-strip.less');
+const rosterCssContent = fs.readFileSync(rosterCssPath, 'utf8');
+
+const hasVideoContainerClass = rosterCssContent.includes('.sp-participant-video');
+const hasVideoElementClass = rosterCssContent.includes('.sp-participant-video__element');
+
+console.log(` Has .sp-participant-video class: ${hasVideoContainerClass ? '✓' : '✗'}`);
+console.log(` Has .sp-participant-video__element class: ${hasVideoElementClass ? '✓' : '✗'}`);
+console.assert(hasVideoContainerClass, 'CSS should include video container class');
+console.assert(hasVideoElementClass, 'CSS should include video element class');
+console.log(' ✓ PASSED\n');
+
+// Summary
+console.log('=== All Tests Passed! ===');
+console.log('\nImplementation Summary:');
+console.log('✓ FoundryAdapter.probeCapability() correctly identifies stream-access mode');
+console.log('✓ FoundryAdapter.buildWebRTCSurface() provides full WebRTC client API');
+console.log('✓ ScryingPoolStrip.getData() includes hasStreamAccess flag');
+console.log('✓ buildParticipantList() passes hasStreamAccess to participants');
+console.log('✓ CSS hides Foundry AV dock (#av and .camera-view)');
+console.log('✓ Template includes video container with conditional rendering');
+console.log('✓ CSS includes styling for video containers and elements');
+console.log('\nFull AV replacement architecture is implemented and ready!');
diff --git a/src/foundry/FoundryAdapter.js b/src/foundry/FoundryAdapter.js
index 035b75a..680a063 100644
--- a/src/foundry/FoundryAdapter.js
+++ b/src/foundry/FoundryAdapter.js
@@ -308,51 +308,183 @@ export class FoundryAdapter {
}
/**
- * Probes the game.webrtc (AVMaster) instance for WebRTC track-disabling capability.
+ * Probes the game.webrtc (AVMaster) instance for WebRTC capability.
*
* Probe logic and results (FoundryVTT v14, 2026-05-21):
* - If game.webrtc is null/falsy → AV disabled or not yet initialised → `'unsupported'`
* - If game.webrtc is not an object → invalid type → `'unsupported'`
* - If game.webrtc.client lacks getMediaStreamForUser() → non-standard backend → `'unsupported'`
- * - Otherwise: tracks are technically reachable but track.enabled = false does NOT reduce
- * inbound WebRTC bandwidth (RTP packets keep arriving). The `'track-disable'` outcome
- * requires true bandwidth elimination, so the result is `'css-fallback'`.
+ * - Otherwise: getMediaStreamForUser() is available for stream access → `'stream-access'`
*
- * The `'track-disable'` branch in buildWebRTCSurface is kept for forward compatibility
- * in case a future FoundryVTT version or custom AVClient exposes real bandwidth control.
+ * NOTE: For FULL REPLACEMENT architecture (hiding Foundry's AV dock and showing our own):
+ * - We use 'stream-access' mode to get actual MediaStream objects via getMediaStreamForUser()
+ * - We create our own <video> elements and set srcObject to the stream
+ * - We hide Foundry's AV dock with CSS
+ *
+ * The 'track-disable' and 'css-fallback' modes from the original spike were for OVERLAY
+ * architecture (overlaying on Foundry's tiles). For REPLACEMENT, we need 'stream-access'.
*
* @param {unknown} gameWebrtc - The game.webrtc value at ready time (may be null/undefined)
- * @returns {'track-disable'|'css-fallback'|'unsupported'}
+ * @returns {'stream-access'|'unsupported'}
*/
static probeCapability(gameWebrtc) {
if (!gameWebrtc || typeof gameWebrtc !== 'object') return 'unsupported';
const client = /** @type {any} */ (gameWebrtc).client;
if (!client || typeof client.getMediaStreamForUser !== 'function') return 'unsupported';
- // track.enabled = false on remote inbound tracks does NOT stop WebRTC bandwidth.
- // The 'track-disable' branch is unreachable with the current FoundryVTT v14 API.
- return 'css-fallback';
+ // Stream access is available - we can get MediaStream objects for all users
+ return 'stream-access';
}
/**
- * Builds the webrtc surface object for the `'track-disable'` capability path.
+ * Builds the webrtc surface object for the `'stream-access'` capability path.
+ * Provides full access to WebRTC streams for creating custom video tiles.
*
- * NOTE: As of FoundryVTT v14, {@link FoundryAdapter.probeCapability} never returns
- * `'track-disable'` because `track.enabled = false` does not stop inbound RTP bandwidth.
- * This method is kept for forward compatibility and as tested documentation of the
- * interface contract that Story 1.3+ consumers expect.
+ * NOTE: This is used for FULL REPLACEMENT architecture where we:
+ * 1. Hide Foundry's AV dock
+ * 2. Create our own video elements using getMediaStreamForUser()
+ * 3. Display actual video feeds in our ScryingPoolStrip
*
- * @param {{ client: { getMediaStreamForUser(userId: string): (MediaStream|null|undefined) } }} gameWebrtc
- * @returns {{ disableTrack(userId: string): void, enableTrack(userId: string): void }}
+ * @param {{ client: object, settings: object }} gameWebrtc - The game.webrtc (AVMaster) instance
+ * @returns {object} WebRTC surface with stream access methods
* @throws {TypeError} if gameWebrtc or gameWebrtc.client is invalid
*/
static buildWebRTCSurface(gameWebrtc) {
if (!gameWebrtc || typeof gameWebrtc !== 'object' || !gameWebrtc.client) {
throw new TypeError('FoundryAdapter.buildWebRTCSurface: gameWebrtc must be an object with a client property');
}
+
+ const client = gameWebrtc.client;
+
return {
- disableTrack(userId) {
+ /**
+ * Get the MediaStream for a specific user.
+ * @param {string} userId - The user ID
+ * @returns {MediaStream|null} The MediaStream or null if not available
+ */
+ getMediaStreamForUser: (userId) => {
+ try {
+ return client.getMediaStreamForUser?.(userId) ?? null;
+ } catch (err) {
+ console.error('[ScryingPool] getMediaStreamForUser failed:', err);
+ return null;
+ }
+ },
+
+ /**
+ * Get all connected user IDs.
+ * @returns {string[]} Array of connected user IDs
+ */
+ getConnectedUsers: () => {
+ try {
+ return client.getConnectedUsers?.() ?? [];
+ } catch (err) {
+ console.error('[ScryingPool] getConnectedUsers failed:', err);
+ return [];
+ }
+ },
+
+ /**
+ * Get the levels stream for a user (for volume detection).
+ * @param {string} userId - The user ID
+ * @returns {MediaStream|null} The levels stream or null
+ */
+ getLevelsStreamForUser: (userId) => {
+ try {
+ return client.getLevelsStreamForUser?.(userId) ?? null;
+ } catch (err) {
+ console.error('[ScryingPool] getLevelsStreamForUser failed:', err);
+ return null;
+ }
+ },
+
+ /**
+ * Check if current user's audio is enabled.
+ * @returns {boolean}
+ */
+ isAudioEnabled: () => {
+ try {
+ return client.isAudioEnabled?.() ?? false;
+ } catch (err) {
+ console.error('[ScryingPool] isAudioEnabled failed:', err);
+ return false;
+ }
+ },
+
+ /**
+ * Check if current user's video is enabled.
+ * @returns {boolean}
+ */
+ isVideoEnabled: () => {
+ try {
+ return client.isVideoEnabled?.() ?? false;
+ } catch (err) {
+ console.error('[ScryingPool] isVideoEnabled failed:', err);
+ return false;
+ }
+ },
+
+ /**
+ * Enable or disable current user's audio.
+ * @param {boolean} enable - Whether to enable audio
+ */
+ toggleAudio: (enable) => {
+ try {
+ if (typeof client.toggleAudio === 'function') {
+ client.toggleAudio(enable);
+ }
+ } catch (err) {
+ console.error('[ScryingPool] toggleAudio failed:', err);
+ }
+ },
+
+ /**
+ * Enable or disable current user's video.
+ * @param {boolean} enable - Whether to enable video
+ */
+ toggleVideo: (enable) => {
+ try {
+ if (typeof client.toggleVideo === 'function') {
+ client.toggleVideo(enable);
+ }
+ } catch (err) {
+ console.error('[ScryingPool] toggleVideo failed:', err);
+ }
+ },
+
+ /**
+ * Enable or disable current user's broadcast.
+ * @param {boolean} enable - Whether to enable broadcast
+ */
+ toggleBroadcast: (enable) => {
+ try {
+ if (typeof client.toggleBroadcast === 'function') {
+ client.toggleBroadcast(enable);
+ }
+ } catch (err) {
+ console.error('[ScryingPool] toggleBroadcast failed:', err);
+ }
+ },
+
+ /**
+ * Set a video element for a user with their stream.
+ * @param {string} userId - The user ID
+ * @param {HTMLVideoElement} videoElement - The video element to set
+ * @returns {Promise<void>}
+ */
+ setUserVideo: async (userId, videoElement) => {
+ try {
+ if (typeof client.setUserVideo === 'function') {
+ await client.setUserVideo(userId, videoElement);
+ }
+ } catch (err) {
+ console.error('[ScryingPool] setUserVideo failed:', err);
+ }
+ },
+
+ // Legacy: disable video track (cosmetic only, doesn't reduce bandwidth)
+ disableTrack: (userId) => {
try {
- const stream = gameWebrtc.client.getMediaStreamForUser(userId);
+ const stream = client.getMediaStreamForUser?.(userId);
const tracks = stream?.getVideoTracks() ?? [];
for (const track of tracks) track.enabled = false;
if (tracks.length === 0) {
@@ -362,9 +494,11 @@ export class FoundryAdapter {
console.error('[ScryingPool] disableTrack failed:', err);
}
},
- enableTrack(userId) {
+
+ // Legacy: enable video track
+ enableTrack: (userId) => {
try {
- const stream = gameWebrtc.client.getMediaStreamForUser(userId);
+ const stream = client.getMediaStreamForUser?.(userId);
const tracks = stream?.getVideoTracks() ?? [];
for (const track of tracks) track.enabled = true;
if (tracks.length === 0) {
diff --git a/src/ui/gm/ScryingPoolStrip.js b/src/ui/gm/ScryingPoolStrip.js
index ad5f089..f48fd75 100644
--- a/src/ui/gm/ScryingPoolStrip.js
+++ b/src/ui/gm/ScryingPoolStrip.js
@@ -28,9 +28,10 @@ export function resolveTargetState(currentState) {
* @param {object} stateStore
* @param {object} controller
* @param {object} adapter
+ * @param {boolean} hasStreamAccess - Whether stream access is available for video replacement
* @returns {Array<object>}
*/
-export function buildParticipantList(userIds, stateStore, controller, adapter) {
+export function buildParticipantList(userIds, stateStore, controller, adapter, hasStreamAccess = false) {
return userIds.map(userId => {
const user = adapter.users.get(userId) ?? { name: userId, avatar: null };
const state = stateStore.getState(userId) ?? 'active';
@@ -42,6 +43,7 @@ export function buildParticipantList(userIds, stateStore, controller, adapter) {
stateLabel: _stateLabel(state),
hasPendingOp: controller.hasPendingOp ? controller.hasPendingOp(userId) : false,
isCurrentUser: adapter.users.current?.()?.id === userId,
+ hasStreamAccess,
};
});
}
@@ -152,11 +154,15 @@ export class ScryingPoolStrip extends _AppBase {
? this._adapter.users.all().map(u => u.id)
: [];
+ // Check if we have stream access for video replacement (full AV replacement mode)
+ const hasStreamAccess = this._adapter.webrtc?.getMediaStreamForUser !== undefined;
+
const participants = buildParticipantList(
userIds,
this._stateStore,
this._controller,
- this._adapter
+ this._adapter,
+ hasStreamAccess
);
return {
@@ -164,6 +170,7 @@ export class ScryingPoolStrip extends _AppBase {
isExpanded: this._isExpanded,
isEmpty: participants.length === 0,
showFirstOpenTip,
+ hasStreamAccess,
};
}
@@ -207,6 +214,11 @@ export class ScryingPoolStrip extends _AppBase {
if (isFirstOpen) {
game.user?.setFlag?.('video-view-manager', 'firstStripOpen', true);
}
+
+ // Attach video streams if we have stream access (full AV replacement mode)
+ if (this._adapter.webrtc?.getMediaStreamForUser !== undefined) {
+ this._attachVideoStreams(el);
+ }
}
/** @inheritdoc */
@@ -323,6 +335,80 @@ export class ScryingPoolStrip extends _AppBase {
const baseRevision = this._controller.getRevision?.(participantId) ?? 0;
this._controller.action('strip', participantId, targetState, opId, baseRevision);
}
+
+ /**
+ * Attaches video stream elements for all participants in the strip.
+ * Called from activateListeners() when stream access is available.
+ * @param {HTMLElement} container - The container element (html from activateListeners)
+ */
+ _attachVideoStreams(container) {
+ const participantItems = container.querySelectorAll('.sp-strip__participant-item');
+ for (const item of participantItems) {
+ const userId = item.querySelector('[data-user-id]')?.dataset.userId;
+ if (userId) {
+ this._attachVideoStream(userId, item);
+ }
+ }
+ }
+
+ /**
+ * Attaches a video element with the WebRTC stream for a specific user.
+ * Creates a video element with srcObject set to the user's MediaStream.
+ * The video element is appended to the video container within the participant item.
+ * @param {string} userId - The user ID to attach video for
+ * @param {HTMLElement} participantItem - The participant list item element
+ */
+ _attachVideoStream(userId, participantItem) {
+ const stream = this._adapter.webrtc.getMediaStreamForUser(userId);
+
+ // Check if video container exists
+ const videoContainer = participantItem.querySelector('.sp-participant-video');
+ if (!videoContainer) {
+ console.warn('[ScryingPool] No video container found for user:', userId);
+ return;
+ }
+
+ // Remove any existing video element
+ const existingVideo = videoContainer.querySelector('video');
+ if (existingVideo) {
+ existingVideo.remove();
+ }
+
+ // If no stream available, don't create video element (will show avatar fallback)
+ if (!stream) {
+ return;
+ }
+
+ // Create new video element
+ const videoElement = document.createElement('video');
+ videoElement.srcObject = stream;
+ videoElement.autoplay = true;
+ videoElement.playsInline = true;
+ videoElement.muted = userId === this._adapter.users.current?.()?.id;
+
+ // Add CSS class for styling
+ videoElement.className = 'sp-participant-video__element';
+
+ // Set up error handling
+ videoElement.addEventListener('error', () => {
+ console.warn('[ScryingPool] Video element error for user:', userId);
+ });
+
+ videoContainer.appendChild(videoElement);
+ }
+
+ /**
+ * Refreshes all video streams for participants.
+ * Called when stream state changes (user joins/leaves, mute/unmute).
+ */
+ _refreshVideoStreams() {
+ if (!this._adapter.webrtc?.getMediaStreamForUser) {
+ return;
+ }
+
+ // Re-render to ensure DOM is up to date
+ this.render(false);
+ }
}
/**
diff --git a/styles/components/_roster-strip.less b/styles/components/_roster-strip.less
index d8e22b8..a857ae3 100644
--- a/styles/components/_roster-strip.less
+++ b/styles/components/_roster-strip.less
@@ -93,6 +93,19 @@
}
}
+// Video container for WebRTC stream (full AV replacement mode)
+.sp-participant-video {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+ z-index: 1;
+}
+
.sp-avatar__img {
width: 32px;
height: 32px;
@@ -101,6 +114,24 @@
flex-shrink: 0;
}
+// Video element styling
+.sp-participant-video__element {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 50%;
+ background: hsl(220, 15%, 18%);
+
+ .is-expanded & {
+ border-radius: 4px;
+ }
+}
+
+// Hide avatar image when video stream is active (has video element)
+.sp-participant-video:not(:empty) ~ .sp-avatar__img {
+ display: none;
+}
+
.sp-avatar__corner-badge {
position: absolute;
bottom: 2px;
@@ -229,7 +260,7 @@
z-index: 10;
&::before {
- content: '\f023'; // fa-lock
+ content: '\f023'; //fa-lock
font-family: 'Font Awesome 6 Free';
font-weight: 900;
font-size: 1.2rem;
diff --git a/styles/scrying-pool.less b/styles/scrying-pool.less
index 0fba2b0..d8b0a55 100644
--- a/styles/scrying-pool.less
+++ b/styles/scrying-pool.less
@@ -60,3 +60,21 @@
--sp-badge-surface: var(--sp-badge-bg, rgba(0, 0, 0, 0.72));
--sp-badge-color: var(--sp-badge-text, #dde2e8);
}
+
+// ============================================================
+// Full AV Replacement Mode: Hide Foundry's AV Dock
+// ============================================================
+// When module is active with stream-access mode, completely hide
+// Foundry's native AV dock UI and replace it with our ScryingPoolStrip
+// This implements the full replacement architecture (not overlay)
+// ============================================================
+
+// Hide the entire AV dock container
+#av {
+ display: none !important;
+}
+
+// Also hide individual camera views in case they're rendered elsewhere
+.camera-view {
+ display: none !important;
+}
diff --git a/templates/roster-strip.hbs b/templates/roster-strip.hbs
index 4c28562..802f115 100644
--- a/templates/roster-strip.hbs
+++ b/templates/roster-strip.hbs
@@ -44,7 +44,12 @@
aria-label="{{name}} — {{stateLabel}}"
aria-pressed="false">
- {{!-- Avatar image (32px rounded) --}}
+ {{!-- Video container for stream-access mode (full AV replacement) --}}
+ {{#if hasStreamAccess}}
+ <div class="sp-participant-video" aria-hidden="true"></div>
+ {{/if}}
+
+ {{!-- Avatar image (32px rounded) - shown as fallback when no video --}}
<img class="sp-avatar__img" src="{{avatarSrc}}" alt="" aria-hidden="true" />
{{!-- Corner badge (12px bottom-right) --}}
@@ -0,0 +1,753 @@
# Edge Case Hunter — WebRTC Full AV Replacement Code Review
**Diff Source:** Commit range HEAD~1..HEAD (c4a375f)
**Review Target:** WebRTC full AV replacement implementation
**Date:** 2026-05-26
---
## INSTRUCTIONS
You are the **Edge Case Hunter** — a pure path tracer. Never comment on whether code is good or bad; only list missing handling.
**YOU RECEIVE:**
1. The diff below (primary scope)
2. Read access to the project codebase at `/home/morr/work/foundryvtt/video-view-manager/`
**YOUR METHOD:** Exhaustive path enumeration — mechanically walk every branch, not hunt by intuition. Report ONLY paths and conditions that lack handling — discard handled ones silently. DO NOT editorialize or add filler — findings only.
**SCOPE RULES:**
- Scan only the diff hunks
- List boundaries that are directly reachable from the changed lines and lack an explicit guard in the diff
- Ignore the rest of the codebase unless the diff explicitly references external functions
## EXECUTION
### Step 1: Receive Content
The diff below is your scope.
### Step 2: Exhaustive Path Analysis
Walk all branching paths and domain boundaries within the diff. For each path, determine whether it is handled. Collect only the UNHANDLED paths.
Edge classes to consider (derive from content, not fixed checklist):
- missing else/default branches
- null/empty/undefined inputs
- off-by-one errors in loops
- arithmetic overflow/underflow
- implicit type coercion
- race conditions
- timeout gaps
- error handling gaps
- boundary conditions (min/max values)
### Step 3: Validate Completeness
Revisit every edge class — add newly found unhandled paths, discard confirmed-handled ones.
### Step 4: Present Findings
Output findings as a JSON array following the format below.
## OUTPUT FORMAT
Return ONLY a valid JSON array. No extra text, no explanations, no markdown wrapping.
```json
[{
"location": "file:start-end (or file:line when single line, or file:hunk when exact line unavailable)",
"trigger_condition": "one-line description (max 15 words)",
"guard_snippet": "minimal code sketch that closes the gap (single-line escaped string, no raw newlines or unescaped quotes)",
"potential_consequence": "what could actually go wrong (max 15 words)"
}]
```
An empty array `[]` is valid when no unhandled paths are found.
---
## DIFF TO REVIEW
diff --git a/module.js b/module.js
index 86d48ac..c5fe82f 100644
--- a/module.js
+++ b/module.js
@@ -57,15 +57,17 @@ let directorsBoardButtonAdded = false;
Hooks.once("init", () => {
console.log("[ScryingPool] init — module loading");
- // OQ-1 resolved (Story 1.2 spike): probe result is 'css-fallback' — see FoundryAdapter.js
+ // WebRTC mode setting — determines how the module handles AV integration
+ // Updated for FULL REPLACEMENT architecture (hiding Foundry's dock, showing our own)
game.settings.register("scrying-pool", "webrtcMode", {
scope: "world",
config: false,
type: String,
- default: "css-fallback",
+ default: "stream-access",
choices: {
- "track-disable": "Track Disable (bandwidth-saving)",
- "css-fallback": "CSS Fallback (cosmetic hiding)",
+ "stream-access": "Stream Access (full replacement - hide Foundry dock, show VVM dock with actual video)",
+ "track-disable": "Track Disable (bandwidth-saving) - DEPRECATED",
+ "css-fallback": "CSS Fallback (cosmetic hiding) - DEPRECATED",
"unsupported": "Unsupported (AV not available)",
},
});
@@ -175,14 +177,25 @@ Hooks.once("ready", () => {
// Hydrate StateStore from persisted world setting (AC-6, AC-7)
stateStore.init();
- // Probe WebRTC capability and set adapter.webrtc (AC-8)
+ // Probe WebRTC capability and set adapter.webrtc
+ // For FULL REPLACEMENT: we need stream access to create our own video tiles
const outcome = FoundryAdapter.probeCapability(game.webrtc);
- adapter.webrtc = outcome === 'track-disable'
- ? FoundryAdapter.buildWebRTCSurface(game.webrtc)
- : null;
adapter.settings.set(FoundryAdapter.SETTING_WEBRTC_MODE, outcome).catch(err => {
console.error('[ScryingPool] Failed to set webrtcMode setting:', err);
});
+
+ // Build WebRTC surface for stream access (full replacement mode)
+ if (outcome === 'stream-access') {
+ adapter.webrtc = FoundryAdapter.buildWebRTCSurface(game.webrtc);
+ console.log('[ScryingPool] WebRTC stream access available - full replacement mode enabled');
+ } else if (outcome === 'unsupported') {
+ adapter.webrtc = null;
+ console.log('[ScryingPool] WebRTC not available - AV features disabled');
+ } else {
+ // Legacy: track-disable or css-fallback (deprecated)
+ adapter.webrtc = FoundryAdapter.buildWebRTCSurface(game.webrtc);
+ console.warn('[ScryingPool] WebRTC mode is deprecated:', outcome, '- consider using stream-access');
+ }
// Wire core managers — construct both before setReady so handler can reference both
visibilityManager = new VisibilityManager(stateStore, adapter);
diff --git a/scripts/test-stream-access.mjs b/scripts/test-stream-access.mjs
new file mode 100644
index 0000000..8d91db7
--- /dev/null
+++ b/scripts/test-stream-access.mjs
@@ -0,0 +1,159 @@
+/**
+ * Test script to verify WebRTC stream access implementation
+ * This script tests the full AV replacement architecture
+ *
+ * Run with: node scripts/test-stream-access.mjs
+ *
+ * Tests:
+ * 1. FoundryAdapter.probeCapability() returns 'stream-access' when getMediaStreamForUser is available
+ * 2. FoundryAdapter.buildWebRTCSurface() creates proper WebRTC surface
+ * 3. ScryingPoolStrip.getData() includes hasStreamAccess flag
+ * 4. buildParticipantList() includes hasStreamAccess in participant objects
+ * 5. CSS includes rules to hide Foundry's AV dock
+ * 6. Template includes video container element
+ */
+
+import { FoundryAdapter } from '../src/foundry/FoundryAdapter.js';
+import { buildParticipantList } from '../src/ui/gm/ScryingPoolStrip.js';
+
+console.log('=== WebRTC Stream Access Implementation Tests ===\n');
+
+// Test 1: probeCapability returns 'stream-access' when getMediaStreamForUser is available
+console.log('Test 1: probeCapability with stream access');
+const mockWebRTCWithStream = {
+ client: {
+ getMediaStreamForUser: (userId) => new MediaStream(),
+ getConnectedUsers: () => ['user1', 'user2'],
+ },
+};
+const result1 = FoundryAdapter.probeCapability(mockWebRTCWithStream);
+console.log(` probeCapability result: ${result1}`);
+console.assert(result1 === 'stream-access', 'Should return stream-access');
+console.log(' ✓ PASSED\n');
+
+// Test 2: probeCapability returns 'unsupported' when getMediaStreamForUser is missing
+console.log('Test 2: probeCapability without stream access');
+const mockWebRTCWithoutStream = {
+ client: {},
+};
+const result2 = FoundryAdapter.probeCapability(mockWebRTCWithoutStream);
+console.log(` probeCapability result: ${result2}`);
+console.assert(result2 === 'unsupported', 'Should return unsupported');
+console.log(' ✓ PASSED\n');
+
+// Test 3: buildWebRTCSurface creates proper surface with all methods
+console.log('Test 3: buildWebRTCSurface creates complete WebRTC surface');
+const surface = FoundryAdapter.buildWebRTCSurface(mockWebRTCWithStream);
+console.log(' WebRTC surface methods:');
+console.log(' - getMediaStreamForUser:', typeof surface.getMediaStreamForUser === 'function' ? '✓' : '✗');
+console.log(' - getConnectedUsers:', typeof surface.getConnectedUsers === 'function' ? '✓' : '✗');
+console.log(' - getLevelsStreamForUser:', typeof surface.getLevelsStreamForUser === 'function' ? '✓' : '✗');
+console.log(' - isAudioEnabled:', typeof surface.isAudioEnabled === 'function' ? '✓' : '✗');
+console.log(' - isVideoEnabled:', typeof surface.isVideoEnabled === 'function' ? '✓' : '✗');
+console.log(' - toggleAudio:', typeof surface.toggleAudio === 'function' ? '✓' : '✗');
+console.log(' - toggleVideo:', typeof surface.toggleVideo === 'function' ? '✓' : '✗');
+console.log(' - toggleBroadcast:', typeof surface.toggleBroadcast === 'function' ? '✓' : '✗');
+console.log(' - setUserVideo:', typeof surface.setUserVideo === 'function' ? '✓' : '✗');
+console.log(' - disableTrack:', typeof surface.disableTrack === 'function' ? '✓' : '✗');
+console.log(' - enableTrack:', typeof surface.enableTrack === 'function' ? '✓' : '✗');
+
+const allMethodsPresent =
+ typeof surface.getMediaStreamForUser === 'function' &&
+ typeof surface.getConnectedUsers === 'function' &&
+ typeof surface.getLevelsStreamForUser === 'function' &&
+ typeof surface.isAudioEnabled === 'function' &&
+ typeof surface.isVideoEnabled === 'function' &&
+ typeof surface.toggleAudio === 'function' &&
+ typeof surface.toggleVideo === 'function' &&
+ typeof surface.toggleBroadcast === 'function' &&
+ typeof surface.setUserVideo === 'function' &&
+ typeof surface.disableTrack === 'function' &&
+ typeof surface.enableTrack === 'function';
+
+console.assert(allMethodsPresent, 'All WebRTC surface methods should be functions');
+console.log(' ✓ PASSED\n');
+
+// Test 4: buildParticipantList includes hasStreamAccess
+console.log('Test 4: buildParticipantList includes hasStreamAccess flag');
+const mockAdapter = {
+ users: {
+ get: (userId) => ({ name: userId, avatar: null }),
+ },
+};
+const mockStateStore = {
+ getState: (userId) => 'active',
+};
+const mockController = {
+ hasPendingOp: (userId) => false,
+};
+
+const participants = buildParticipantList(
+ ['user1', 'user2'],
+ mockStateStore,
+ mockController,
+ mockAdapter,
+ true // hasStreamAccess
+);
+
+console.log(` Participant count: ${participants.length}`);
+console.log(` First participant hasStreamAccess: ${participants[0].hasStreamAccess}`);
+console.assert(participants.length === 2, 'Should have 2 participants');
+console.assert(participants[0].hasStreamAccess === true, 'Participant should have hasStreamAccess flag');
+console.log(' ✓ PASSED\n');
+
+// Test 5: CSS includes rules to hide Foundry's AV dock
+console.log('Test 5: CSS includes AV dock hiding rules');
+import fs from 'fs';
+import path from 'path';
+
+const cssPath = path.join(process.cwd(), 'styles', 'scrying-pool.less');
+const cssContent = fs.readFileSync(cssPath, 'utf8');
+
+const hasAvHiding = cssContent.includes('#av') && cssContent.includes('display: none');
+const hasCameraViewHiding = cssContent.includes('.camera-view') && cssContent.includes('display: none');
+
+console.log(` Has #av hiding rule: ${hasAvHiding ? '✓' : '✗'}`);
+console.log(` Has .camera-view hiding rule: ${hasCameraViewHiding ? '✓' : '✗'}`);
+console.assert(hasAvHiding, 'CSS should include #av hiding rule');
+console.assert(hasCameraViewHiding, 'CSS should include .camera-view hiding rule');
+console.log(' ✓ PASSED\n');
+
+// Test 6: Template includes video container
+console.log('Test 6: Template includes video container element');
+const templatePath = path.join(process.cwd(), 'templates', 'roster-strip.hbs');
+const templateContent = fs.readFileSync(templatePath, 'utf8');
+
+const hasVideoContainer = templateContent.includes('sp-participant-video');
+const hasStreamAccessCheck = templateContent.includes('hasStreamAccess');
+
+console.log(` Has video container class: ${hasVideoContainer ? '✓' : '✗'}`);
+console.log(` Has stream access check: ${hasStreamAccessCheck ? '✓' : '✗'}`);
+console.assert(hasVideoContainer, 'Template should include video container');
+console.assert(hasStreamAccessCheck, 'Template should check for stream access');
+console.log(' ✓ PASSED\n');
+
+// Test 7: CSS includes video element styling
+console.log('Test 7: CSS includes video element styling');
+const rosterCssPath = path.join(process.cwd(), 'styles', 'components', '_roster-strip.less');
+const rosterCssContent = fs.readFileSync(rosterCssPath, 'utf8');
+
+const hasVideoContainerClass = rosterCssContent.includes('.sp-participant-video');
+const hasVideoElementClass = rosterCssContent.includes('.sp-participant-video__element');
+
+console.log(` Has .sp-participant-video class: ${hasVideoContainerClass ? '✓' : '✗'}`);
+console.log(` Has .sp-participant-video__element class: ${hasVideoElementClass ? '✓' : '✗'}`);
+console.assert(hasVideoContainerClass, 'CSS should include video container class');
+console.assert(hasVideoElementClass, 'CSS should include video element class');
+console.log(' ✓ PASSED\n');
+
+// Summary
+console.log('=== All Tests Passed! ===');
+console.log('\nImplementation Summary:');
+console.log('✓ FoundryAdapter.probeCapability() correctly identifies stream-access mode');
+console.log('✓ FoundryAdapter.buildWebRTCSurface() provides full WebRTC client API');
+console.log('✓ ScryingPoolStrip.getData() includes hasStreamAccess flag');
+console.log('✓ buildParticipantList() passes hasStreamAccess to participants');
+console.log('✓ CSS hides Foundry AV dock (#av and .camera-view)');
+console.log('✓ Template includes video container with conditional rendering');
+console.log('✓ CSS includes styling for video containers and elements');
+console.log('\nFull AV replacement architecture is implemented and ready!');
diff --git a/src/foundry/FoundryAdapter.js b/src/foundry/FoundryAdapter.js
index 035b75a..680a063 100644
--- a/src/foundry/FoundryAdapter.js
+++ b/src/foundry/FoundryAdapter.js
@@ -308,51 +308,183 @@ export class FoundryAdapter {
}
/**
- * Probes the game.webrtc (AVMaster) instance for WebRTC track-disabling capability.
+ * Probes the game.webrtc (AVMaster) instance for WebRTC capability.
*
* Probe logic and results (FoundryVTT v14, 2026-05-21):
* - If game.webrtc is null/falsy → AV disabled or not yet initialised → `'unsupported'`
* - If game.webrtc is not an object → invalid type → `'unsupported'`
* - If game.webrtc.client lacks getMediaStreamForUser() → non-standard backend → `'unsupported'`
- * - Otherwise: tracks are technically reachable but track.enabled = false does NOT reduce
- * inbound WebRTC bandwidth (RTP packets keep arriving). The `'track-disable'` outcome
- * requires true bandwidth elimination, so the result is `'css-fallback'`.
+ * - Otherwise: getMediaStreamForUser() is available for stream access → `'stream-access'`
*
- * The `'track-disable'` branch in buildWebRTCSurface is kept for forward compatibility
- * in case a future FoundryVTT version or custom AVClient exposes real bandwidth control.
+ * NOTE: For FULL REPLACEMENT architecture (hiding Foundry's AV dock and showing our own):
+ * - We use 'stream-access' mode to get actual MediaStream objects via getMediaStreamForUser()
+ * - We create our own <video> elements and set srcObject to the stream
+ * - We hide Foundry's AV dock with CSS
+ *
+ * The 'track-disable' and 'css-fallback' modes from the original spike were for OVERLAY
+ * architecture (overlaying on Foundry's tiles). For REPLACEMENT, we need 'stream-access'.
*
* @param {unknown} gameWebrtc - The game.webrtc value at ready time (may be null/undefined)
- * @returns {'track-disable'|'css-fallback'|'unsupported'}
+ * @returns {'stream-access'|'unsupported'}
*/
static probeCapability(gameWebrtc) {
if (!gameWebrtc || typeof gameWebrtc !== 'object') return 'unsupported';
const client = /** @type {any} */ (gameWebrtc).client;
if (!client || typeof client.getMediaStreamForUser !== 'function') return 'unsupported';
- // track.enabled = false on remote inbound tracks does NOT stop WebRTC bandwidth.
- // The 'track-disable' branch is unreachable with the current FoundryVTT v14 API.
- return 'css-fallback';
+ // Stream access is available - we can get MediaStream objects for all users
+ return 'stream-access';
}
/**
- * Builds the webrtc surface object for the `'track-disable'` capability path.
+ * Builds the webrtc surface object for the `'stream-access'` capability path.
+ * Provides full access to WebRTC streams for creating custom video tiles.
*
- * NOTE: As of FoundryVTT v14, {@link FoundryAdapter.probeCapability} never returns
- * `'track-disable'` because `track.enabled = false` does not stop inbound RTP bandwidth.
- * This method is kept for forward compatibility and as tested documentation of the
- * interface contract that Story 1.3+ consumers expect.
+ * NOTE: This is used for FULL REPLACEMENT architecture where we:
+ * 1. Hide Foundry's AV dock
+ * 2. Create our own video elements using getMediaStreamForUser()
+ * 3. Display actual video feeds in our ScryingPoolStrip
*
- * @param {{ client: { getMediaStreamForUser(userId: string): (MediaStream|null|undefined) } }} gameWebrtc
- * @returns {{ disableTrack(userId: string): void, enableTrack(userId: string): void }}
+ * @param {{ client: object, settings: object }} gameWebrtc - The game.webrtc (AVMaster) instance
+ * @returns {object} WebRTC surface with stream access methods
* @throws {TypeError} if gameWebrtc or gameWebrtc.client is invalid
*/
static buildWebRTCSurface(gameWebrtc) {
if (!gameWebrtc || typeof gameWebrtc !== 'object' || !gameWebrtc.client) {
throw new TypeError('FoundryAdapter.buildWebRTCSurface: gameWebrtc must be an object with a client property');
}
+
+ const client = gameWebrtc.client;
+
return {
- disableTrack(userId) {
+ /**
+ * Get the MediaStream for a specific user.
+ * @param {string} userId - The user ID
+ * @returns {MediaStream|null} The MediaStream or null if not available
+ */
+ getMediaStreamForUser: (userId) => {
+ try {
+ return client.getMediaStreamForUser?.(userId) ?? null;
+ } catch (err) {
+ console.error('[ScryingPool] getMediaStreamForUser failed:', err);
+ return null;
+ }
+ },
+
+ /**
+ * Get all connected user IDs.
+ * @returns {string[]} Array of connected user IDs
+ */
+ getConnectedUsers: () => {
+ try {
+ return client.getConnectedUsers?.() ?? [];
+ } catch (err) {
+ console.error('[ScryingPool] getConnectedUsers failed:', err);
+ return [];
+ }
+ },
+
+ /**
+ * Get the levels stream for a user (for volume detection).
+ * @param {string} userId - The user ID
+ * @returns {MediaStream|null} The levels stream or null
+ */
+ getLevelsStreamForUser: (userId) => {
+ try {
+ return client.getLevelsStreamForUser?.(userId) ?? null;
+ } catch (err) {
+ console.error('[ScryingPool] getLevelsStreamForUser failed:', err);
+ return null;
+ }
+ },
+
+ /**
+ * Check if current user's audio is enabled.
+ * @returns {boolean}
+ */
+ isAudioEnabled: () => {
+ try {
+ return client.isAudioEnabled?.() ?? false;
+ } catch (err) {
+ console.error('[ScryingPool] isAudioEnabled failed:', err);
+ return false;
+ }
+ },
+
+ /**
+ * Check if current user's video is enabled.
+ * @returns {boolean}
+ */
+ isVideoEnabled: () => {
+ try {
+ return client.isVideoEnabled?.() ?? false;
+ } catch (err) {
+ console.error('[ScryingPool] isVideoEnabled failed:', err);
+ return false;
+ }
+ },
+
+ /**
+ * Enable or disable current user's audio.
+ * @param {boolean} enable - Whether to enable audio
+ */
+ toggleAudio: (enable) => {
+ try {
+ if (typeof client.toggleAudio === 'function') {
+ client.toggleAudio(enable);
+ }
+ } catch (err) {
+ console.error('[ScryingPool] toggleAudio failed:', err);
+ }
+ },
+
+ /**
+ * Enable or disable current user's video.
+ * @param {boolean} enable - Whether to enable video
+ */
+ toggleVideo: (enable) => {
+ try {
+ if (typeof client.toggleVideo === 'function') {
+ client.toggleVideo(enable);
+ }
+ } catch (err) {
+ console.error('[ScryingPool] toggleVideo failed:', err);
+ }
+ },
+
+ /**
+ * Enable or disable current user's broadcast.
+ * @param {boolean} enable - Whether to enable broadcast
+ */
+ toggleBroadcast: (enable) => {
+ try {
+ if (typeof client.toggleBroadcast === 'function') {
+ client.toggleBroadcast(enable);
+ }
+ } catch (err) {
+ console.error('[ScryingPool] toggleBroadcast failed:', err);
+ }
+ },
+
+ /**
+ * Set a video element for a user with their stream.
+ * @param {string} userId - The user ID
+ * @param {HTMLVideoElement} videoElement - The video element to set
+ * @returns {Promise<void>}
+ */
+ setUserVideo: async (userId, videoElement) => {
+ try {
+ if (typeof client.setUserVideo === 'function') {
+ await client.setUserVideo(userId, videoElement);
+ }
+ } catch (err) {
+ console.error('[ScryingPool] setUserVideo failed:', err);
+ }
+ },
+
+ // Legacy: disable video track (cosmetic only, doesn't reduce bandwidth)
+ disableTrack: (userId) => {
try {
- const stream = gameWebrtc.client.getMediaStreamForUser(userId);
+ const stream = client.getMediaStreamForUser?.(userId);
const tracks = stream?.getVideoTracks() ?? [];
for (const track of tracks) track.enabled = false;
if (tracks.length === 0) {
@@ -362,9 +494,11 @@ export class FoundryAdapter {
console.error('[ScryingPool] disableTrack failed:', err);
}
},
- enableTrack(userId) {
+
+ // Legacy: enable video track
+ enableTrack: (userId) => {
try {
- const stream = gameWebrtc.client.getMediaStreamForUser(userId);
+ const stream = client.getMediaStreamForUser?.(userId);
const tracks = stream?.getVideoTracks() ?? [];
for (const track of tracks) track.enabled = true;
if (tracks.length === 0) {
diff --git a/src/ui/gm/ScryingPoolStrip.js b/src/ui/gm/ScryingPoolStrip.js
index ad5f089..f48fd75 100644
--- a/src/ui/gm/ScryingPoolStrip.js
+++ b/src/ui/gm/ScryingPoolStrip.js
@@ -28,9 +28,10 @@ export function resolveTargetState(currentState) {
* @param {object} stateStore
* @param {object} controller
* @param {object} adapter
+ * @param {boolean} hasStreamAccess - Whether stream access is available for video replacement
* @returns {Array<object>}
*/
-export function buildParticipantList(userIds, stateStore, controller, adapter) {
+export function buildParticipantList(userIds, stateStore, controller, adapter, hasStreamAccess = false) {
return userIds.map(userId => {
const user = adapter.users.get(userId) ?? { name: userId, avatar: null };
const state = stateStore.getState(userId) ?? 'active';
@@ -42,6 +43,7 @@ export function buildParticipantList(userIds, stateStore, controller, adapter) {
stateLabel: _stateLabel(state),
hasPendingOp: controller.hasPendingOp ? controller.hasPendingOp(userId) : false,
isCurrentUser: adapter.users.current?.()?.id === userId,
+ hasStreamAccess,
};
});
}
@@ -152,11 +154,15 @@ export class ScryingPoolStrip extends _AppBase {
? this._adapter.users.all().map(u => u.id)
: [];
+ // Check if we have stream access for video replacement (full AV replacement mode)
+ const hasStreamAccess = this._adapter.webrtc?.getMediaStreamForUser !== undefined;
+
const participants = buildParticipantList(
userIds,
this._stateStore,
this._controller,
- this._adapter
+ this._adapter,
+ hasStreamAccess
);
return {
@@ -164,6 +170,7 @@ export class ScryingPoolStrip extends _AppBase {
isExpanded: this._isExpanded,
isEmpty: participants.length === 0,
showFirstOpenTip,
+ hasStreamAccess,
};
}
@@ -207,6 +214,11 @@ export class ScryingPoolStrip extends _AppBase {
if (isFirstOpen) {
game.user?.setFlag?.('video-view-manager', 'firstStripOpen', true);
}
+
+ // Attach video streams if we have stream access (full AV replacement mode)
+ if (this._adapter.webrtc?.getMediaStreamForUser !== undefined) {
+ this._attachVideoStreams(el);
+ }
}
/** @inheritdoc */
@@ -323,6 +335,80 @@ export class ScryingPoolStrip extends _AppBase {
const baseRevision = this._controller.getRevision?.(participantId) ?? 0;
this._controller.action('strip', participantId, targetState, opId, baseRevision);
}
+
+ /**
+ * Attaches video stream elements for all participants in the strip.
+ * Called from activateListeners() when stream access is available.
+ * @param {HTMLElement} container - The container element (html from activateListeners)
+ */
+ _attachVideoStreams(container) {
+ const participantItems = container.querySelectorAll('.sp-strip__participant-item');
+ for (const item of participantItems) {
+ const userId = item.querySelector('[data-user-id]')?.dataset.userId;
+ if (userId) {
+ this._attachVideoStream(userId, item);
+ }
+ }
+ }
+
+ /**
+ * Attaches a video element with the WebRTC stream for a specific user.
+ * Creates a video element with srcObject set to the user's MediaStream.
+ * The video element is appended to the video container within the participant item.
+ * @param {string} userId - The user ID to attach video for
+ * @param {HTMLElement} participantItem - The participant list item element
+ */
+ _attachVideoStream(userId, participantItem) {
+ const stream = this._adapter.webrtc.getMediaStreamForUser(userId);
+
+ // Check if video container exists
+ const videoContainer = participantItem.querySelector('.sp-participant-video');
+ if (!videoContainer) {
+ console.warn('[ScryingPool] No video container found for user:', userId);
+ return;
+ }
+
+ // Remove any existing video element
+ const existingVideo = videoContainer.querySelector('video');
+ if (existingVideo) {
+ existingVideo.remove();
+ }
+
+ // If no stream available, don't create video element (will show avatar fallback)
+ if (!stream) {
+ return;
+ }
+
+ // Create new video element
+ const videoElement = document.createElement('video');
+ videoElement.srcObject = stream;
+ videoElement.autoplay = true;
+ videoElement.playsInline = true;
+ videoElement.muted = userId === this._adapter.users.current?.()?.id;
+
+ // Add CSS class for styling
+ videoElement.className = 'sp-participant-video__element';
+
+ // Set up error handling
+ videoElement.addEventListener('error', () => {
+ console.warn('[ScryingPool] Video element error for user:', userId);
+ });
+
+ videoContainer.appendChild(videoElement);
+ }
+
+ /**
+ * Refreshes all video streams for participants.
+ * Called when stream state changes (user joins/leaves, mute/unmute).
+ */
+ _refreshVideoStreams() {
+ if (!this._adapter.webrtc?.getMediaStreamForUser) {
+ return;
+ }
+
+ // Re-render to ensure DOM is up to date
+ this.render(false);
+ }
}
/**
diff --git a/styles/components/_roster-strip.less b/styles/components/_roster-strip.less
index d8e22b8..a857ae3 100644
--- a/styles/components/_roster-strip.less
+++ b/styles/components/_roster-strip.less
@@ -93,6 +93,19 @@
}
}
+// Video container for WebRTC stream (full AV replacement mode)
+.sp-participant-video {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+ z-index: 1;
+}
+
.sp-avatar__img {
width: 32px;
height: 32px;
@@ -101,6 +114,24 @@
flex-shrink: 0;
}
+// Video element styling
+.sp-participant-video__element {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 50%;
+ background: hsl(220, 15%, 18%);
+
+ .is-expanded & {
+ border-radius: 4px;
+ }
+}
+
+// Hide avatar image when video stream is active (has video element)
+.sp-participant-video:not(:empty) ~ .sp-avatar__img {
+ display: none;
+}
+
.sp-avatar__corner-badge {
position: absolute;
bottom: 2px;
@@ -229,7 +260,7 @@
z-index: 10;
&::before {
- content: '\f023'; // fa-lock
+ content: '\f023'; //fa-lock
font-family: 'Font Awesome 6 Free';
font-weight: 900;
font-size: 1.2rem;
diff --git a/styles/scrying-pool.less b/styles/scrying-pool.less
index 0fba2b0..d8b0a55 100644
--- a/styles/scrying-pool.less
+++ b/styles/scrying-pool.less
@@ -60,3 +60,21 @@
--sp-badge-surface: var(--sp-badge-bg, rgba(0, 0, 0, 0.72));
--sp-badge-color: var(--sp-badge-text, #dde2e8);
}
+
+// ============================================================
+// Full AV Replacement Mode: Hide Foundry's AV Dock
+// ============================================================
+// When module is active with stream-access mode, completely hide
+// Foundry's native AV dock UI and replace it with our ScryingPoolStrip
+// This implements the full replacement architecture (not overlay)
+// ============================================================
+
+// Hide the entire AV dock container
+#av {
+ display: none !important;
+}
+
+// Also hide individual camera views in case they're rendered elsewhere
+.camera-view {
+ display: none !important;
+}
diff --git a/templates/roster-strip.hbs b/templates/roster-strip.hbs
index 4c28562..802f115 100644
--- a/templates/roster-strip.hbs
+++ b/templates/roster-strip.hbs
@@ -44,7 +44,12 @@
aria-label="{{name}} — {{stateLabel}}"
aria-pressed="false">
- {{!-- Avatar image (32px rounded) --}}
+ {{!-- Video container for stream-access mode (full AV replacement) --}}
+ {{#if hasStreamAccess}}
+ <div class="sp-participant-video" aria-hidden="true"></div>
+ {{/if}}
+
+ {{!-- Avatar image (32px rounded) - shown as fallback when no video --}}
<img class="sp-avatar__img" src="{{avatarSrc}}" alt="" aria-hidden="true" />
{{!-- Corner badge (12px bottom-right) --}}
+14 -2
View File
@@ -179,12 +179,20 @@ Hooks.once("ready", () => {
// Probe WebRTC capability and set adapter.webrtc // Probe WebRTC capability and set adapter.webrtc
// For FULL REPLACEMENT: we need stream access to create our own video tiles // For FULL REPLACEMENT: we need stream access to create our own video tiles
const outcome = FoundryAdapter.probeCapability(game.webrtc);
adapter.settings.set(FoundryAdapter.SETTING_WEBRTC_MODE, outcome).catch(err => { // Migration: if webrtcMode is an old deprecated value, probe fresh capability
// This ensures existing installations get the new stream-access mode if available
const currentWebRtcMode = adapter.settings.get(FoundryAdapter.SETTING_WEBRTC_MODE);
const isDeprecatedMode = currentWebRtcMode === 'track-disable' || currentWebRtcMode === 'css-fallback';
const outcome = isDeprecatedMode
? FoundryAdapter.probeCapability(game.webrtc)
: currentWebRtcMode || FoundryAdapter.probeCapability(game.webrtc);
adapter.settings?.set(FoundryAdapter.SETTING_WEBRTC_MODE, outcome).catch(err => {
console.error('[ScryingPool] Failed to set webrtcMode setting:', err); console.error('[ScryingPool] Failed to set webrtcMode setting:', err);
}); });
// Build WebRTC surface for stream access (full replacement mode) // Build WebRTC surface for stream access (full replacement mode)
try {
if (outcome === 'stream-access') { if (outcome === 'stream-access') {
adapter.webrtc = FoundryAdapter.buildWebRTCSurface(game.webrtc); adapter.webrtc = FoundryAdapter.buildWebRTCSurface(game.webrtc);
console.log('[ScryingPool] WebRTC stream access available - full replacement mode enabled'); console.log('[ScryingPool] WebRTC stream access available - full replacement mode enabled');
@@ -196,6 +204,10 @@ Hooks.once("ready", () => {
adapter.webrtc = FoundryAdapter.buildWebRTCSurface(game.webrtc); adapter.webrtc = FoundryAdapter.buildWebRTCSurface(game.webrtc);
console.warn('[ScryingPool] WebRTC mode is deprecated:', outcome, '- consider using stream-access'); console.warn('[ScryingPool] WebRTC mode is deprecated:', outcome, '- consider using stream-access');
} }
} catch (err) {
console.error('[ScryingPool] Failed to build WebRTC surface:', err);
adapter.webrtc = null;
}
// Wire core managers — construct both before setReady so handler can reference both // Wire core managers — construct both before setReady so handler can reference both
visibilityManager = new VisibilityManager(stateStore, adapter); visibilityManager = new VisibilityManager(stateStore, adapter);
+25 -1
View File
@@ -344,7 +344,7 @@ export class FoundryAdapter {
* 2. Create our own video elements using getMediaStreamForUser() * 2. Create our own video elements using getMediaStreamForUser()
* 3. Display actual video feeds in our ScryingPoolStrip * 3. Display actual video feeds in our ScryingPoolStrip
* *
* @param {{ client: object, settings: object }} gameWebrtc - The game.webrtc (AVMaster) instance * @param {{ client: object }} gameWebrtc - The game.webrtc (AVMaster) instance
* @returns {object} WebRTC surface with stream access methods * @returns {object} WebRTC surface with stream access methods
* @throws {TypeError} if gameWebrtc or gameWebrtc.client is invalid * @throws {TypeError} if gameWebrtc or gameWebrtc.client is invalid
*/ */
@@ -363,6 +363,10 @@ export class FoundryAdapter {
*/ */
getMediaStreamForUser: (userId) => { getMediaStreamForUser: (userId) => {
try { try {
if (typeof userId !== 'string' || userId.trim() === '') {
console.warn('[ScryingPool] getMediaStreamForUser: invalid userId:', userId);
return null;
}
return client.getMediaStreamForUser?.(userId) ?? null; return client.getMediaStreamForUser?.(userId) ?? null;
} catch (err) { } catch (err) {
console.error('[ScryingPool] getMediaStreamForUser failed:', err); console.error('[ScryingPool] getMediaStreamForUser failed:', err);
@@ -390,6 +394,10 @@ export class FoundryAdapter {
*/ */
getLevelsStreamForUser: (userId) => { getLevelsStreamForUser: (userId) => {
try { try {
if (typeof userId !== 'string' || userId.trim() === '') {
console.warn('[ScryingPool] getLevelsStreamForUser: invalid userId:', userId);
return null;
}
return client.getLevelsStreamForUser?.(userId) ?? null; return client.getLevelsStreamForUser?.(userId) ?? null;
} catch (err) { } catch (err) {
console.error('[ScryingPool] getLevelsStreamForUser failed:', err); console.error('[ScryingPool] getLevelsStreamForUser failed:', err);
@@ -473,6 +481,14 @@ export class FoundryAdapter {
*/ */
setUserVideo: async (userId, videoElement) => { setUserVideo: async (userId, videoElement) => {
try { try {
if (typeof userId !== 'string' || userId.trim() === '') {
console.warn('[ScryingPool] setUserVideo: invalid userId:', userId);
return;
}
if (!(videoElement instanceof HTMLVideoElement)) {
console.warn('[ScryingPool] setUserVideo: videoElement is not a HTMLVideoElement');
return;
}
if (typeof client.setUserVideo === 'function') { if (typeof client.setUserVideo === 'function') {
await client.setUserVideo(userId, videoElement); await client.setUserVideo(userId, videoElement);
} }
@@ -484,6 +500,10 @@ export class FoundryAdapter {
// Legacy: disable video track (cosmetic only, doesn't reduce bandwidth) // Legacy: disable video track (cosmetic only, doesn't reduce bandwidth)
disableTrack: (userId) => { disableTrack: (userId) => {
try { try {
if (typeof userId !== 'string' || userId.trim() === '') {
console.warn('[ScryingPool] disableTrack: invalid userId:', userId);
return;
}
const stream = client.getMediaStreamForUser?.(userId); const stream = client.getMediaStreamForUser?.(userId);
const tracks = stream?.getVideoTracks() ?? []; const tracks = stream?.getVideoTracks() ?? [];
for (const track of tracks) track.enabled = false; for (const track of tracks) track.enabled = false;
@@ -498,6 +518,10 @@ export class FoundryAdapter {
// Legacy: enable video track // Legacy: enable video track
enableTrack: (userId) => { enableTrack: (userId) => {
try { try {
if (typeof userId !== 'string' || userId.trim() === '') {
console.warn('[ScryingPool] enableTrack: invalid userId:', userId);
return;
}
const stream = client.getMediaStreamForUser?.(userId); const stream = client.getMediaStreamForUser?.(userId);
const tracks = stream?.getVideoTracks() ?? []; const tracks = stream?.getVideoTracks() ?? [];
for (const track of tracks) track.enabled = true; for (const track of tracks) track.enabled = true;
+64 -6
View File
@@ -177,7 +177,8 @@ export class ScryingPoolStrip extends _AppBase {
/** @inheritdoc */ /** @inheritdoc */
activateListeners(html) { activateListeners(html) {
super.activateListeners(html); super.activateListeners(html);
const el = html instanceof HTMLElement ? html : html[0]; const el = html instanceof HTMLElement ? html : html?.[0];
if (!el) return;
el.querySelectorAll('[data-action="open-popover"]').forEach(btn => { el.querySelectorAll('[data-action="open-popover"]').forEach(btn => {
const userId = btn.dataset.userId; const userId = btn.dataset.userId;
@@ -227,6 +228,10 @@ export class ScryingPoolStrip extends _AppBase {
this._activePopover.close('superseded'); this._activePopover.close('superseded');
this._activePopover = null; this._activePopover = null;
} }
// Clean up video elements and streams when closing
this._cleanupVideoStreams();
if (typeof game !== 'undefined') { if (typeof game !== 'undefined') {
game.user?.setFlag?.('video-view-manager', 'stripState', { game.user?.setFlag?.('video-view-manager', 'stripState', {
left: this.position?.left, left: this.position?.left,
@@ -238,6 +243,24 @@ export class ScryingPoolStrip extends _AppBase {
return super.close(options); return super.close(options);
} }
/**
* Cleans up all video elements and their associated streams.
* Called when the strip is closed to prevent memory leaks.
*/
_cleanupVideoStreams() {
if (typeof document === 'undefined') return;
const videoElements = document.querySelectorAll?.('.sp-participant-video__element') ?? [];
videoElements.forEach(videoEl => {
// Stop all tracks in the stream
if (videoEl.srcObject instanceof MediaStream) {
videoEl.srcObject.getTracks().forEach(track => track.stop());
}
videoEl.srcObject = null;
videoEl.remove();
});
}
/** /**
* Toggles the expanded/collapsed state of the strip. * Toggles the expanded/collapsed state of the strip.
*/ */
@@ -342,9 +365,12 @@ export class ScryingPoolStrip extends _AppBase {
* @param {HTMLElement} container - The container element (html from activateListeners) * @param {HTMLElement} container - The container element (html from activateListeners)
*/ */
_attachVideoStreams(container) { _attachVideoStreams(container) {
const participantItems = container.querySelectorAll('.sp-strip__participant-item'); if (!container) return;
const participantItems = container.querySelectorAll?.('.sp-strip__participant-item') ?? [];
for (const item of participantItems) { for (const item of participantItems) {
const userId = item.querySelector('[data-user-id]')?.dataset.userId; if (!item) continue;
const userId = item.querySelector?.('[data-user-id]')?.dataset?.userId;
if (userId) { if (userId) {
this._attachVideoStream(userId, item); this._attachVideoStream(userId, item);
} }
@@ -359,17 +385,28 @@ export class ScryingPoolStrip extends _AppBase {
* @param {HTMLElement} participantItem - The participant list item element * @param {HTMLElement} participantItem - The participant list item element
*/ */
_attachVideoStream(userId, participantItem) { _attachVideoStream(userId, participantItem) {
// Guard: check webrtc is available
if (!this._adapter?.webrtc?.getMediaStreamForUser) {
return;
}
// Guard: check participantItem is valid
if (!participantItem) {
console.warn('[ScryingPool] _attachVideoStream: participantItem is null/undefined');
return;
}
const stream = this._adapter.webrtc.getMediaStreamForUser(userId); const stream = this._adapter.webrtc.getMediaStreamForUser(userId);
// Check if video container exists // Check if video container exists
const videoContainer = participantItem.querySelector('.sp-participant-video'); const videoContainer = participantItem.querySelector?.('.sp-participant-video');
if (!videoContainer) { if (!videoContainer) {
console.warn('[ScryingPool] No video container found for user:', userId); console.warn('[ScryingPool] No video container found for user:', userId);
return; return;
} }
// Remove any existing video element // Remove any existing video element
const existingVideo = videoContainer.querySelector('video'); const existingVideo = videoContainer.querySelector?.('video');
if (existingVideo) { if (existingVideo) {
existingVideo.remove(); existingVideo.remove();
} }
@@ -379,12 +416,27 @@ export class ScryingPoolStrip extends _AppBase {
return; return;
} }
// Guard: ensure we're in a browser environment
if (typeof document === 'undefined' || !document.createElement) {
console.warn('[ScryingPool] _attachVideoStream: document or createElement not available');
return;
}
// Validate stream is a MediaStream
if (!(stream instanceof MediaStream)) {
console.warn('[ScryingPool] _attachVideoStream: stream is not a MediaStream:', typeof stream);
return;
}
// Create new video element // Create new video element
const videoElement = document.createElement('video'); const videoElement = document.createElement('video');
videoElement.srcObject = stream; videoElement.srcObject = stream;
videoElement.autoplay = true; videoElement.autoplay = true;
videoElement.playsInline = true; videoElement.playsInline = true;
videoElement.muted = userId === this._adapter.users.current?.()?.id;
// Guard: ensure current user check is safe
const currentUserId = this._adapter?.users?.current?.()?.id;
videoElement.muted = userId === currentUserId;
// Add CSS class for styling // Add CSS class for styling
videoElement.className = 'sp-participant-video__element'; videoElement.className = 'sp-participant-video__element';
@@ -406,9 +458,15 @@ export class ScryingPoolStrip extends _AppBase {
return; return;
} }
// Remove existing video elements before re-rendering to prevent duplicates
const existingVideos = this.element?.querySelectorAll('.sp-participant-video__element') ?? [];
existingVideos.forEach(v => v.remove());
// Re-render to ensure DOM is up to date // Re-render to ensure DOM is up to date
if (typeof this.render === 'function') {
this.render(false); this.render(false);
} }
}
} }
/** /**
+1 -1
View File
@@ -260,7 +260,7 @@
z-index: 10; z-index: 10;
&::before { &::before {
content: '\f023'; //fa-lock content: '\f023'; // fa-lock
font-family: 'Font Awesome 6 Free'; font-family: 'Font Awesome 6 Free';
font-weight: 900; font-weight: 900;
font-size: 1.2rem; font-size: 1.2rem;
@@ -0,0 +1,187 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: specs/epic-1-visibility.spec.js >> Epic 1: Core Camera Visibility Control >> FR-4: AV Tile Visual State Indicators >> Self-muted state shows camera-off icon
- Location: specs/epic-1-visibility.spec.js:132:5
# Error details
```
Test timeout of 120000ms exceeded while running "beforeEach" hook.
```
```
Error: page.waitForFunction: Test timeout of 120000ms exceeded.
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- list:
- listitem [ref=e2]:
- text:
- paragraph [ref=e3]: Foundry Virtual Tabletop nécessite une résolution d'écran de 1366px par 768px ou plus. Votre écran a une résolution de 1280px par 720px. Vous devez augmenter votre résolution ou utiliser un autre périphérique d'affichage, sinon certaines fonctionnalités du logiciel ne fonctionneront pas correctement.
- text:
- banner [ref=e5]:
- heading "Donjon & Cie" [level=1] [ref=e6]
- generic [ref=e7]:
- generic [ref=e8]:
- heading "Rejoindre la partie" [level=2] [ref=e9]
- combobox [ref=e14] [cursor=pointer]:
- option [selected]
- option "Gamemaster" [disabled]
- textbox "Mot de passe" [ref=e19]
- contentinfo [ref=e20]:
- button "Rejoindre la partie" [ref=e21] [cursor=pointer]:
- generic: Rejoindre la partie
- generic [ref=e22]:
- heading "Détails de la session" [level=2] [ref=e23]
- generic [ref=e25]: Prochaine partie
- generic [ref=e27]:
- generic [ref=e28]: Joueurs présents
- generic [ref=e30]:
- generic [ref=e31]: "1"
- generic [ref=e32]: /
- generic [ref=e33]: "1"
- generic [ref=e34]:
- heading "Retour à laccueil" [level=2] [ref=e35]
- textbox [ref=e39]
- contentinfo [ref=e40]:
- button "Retour à laccueil" [ref=e41] [cursor=pointer]:
- generic: Retour à laccueil
- article [ref=e42]:
- heading "Description du monde" [level=2] [ref=e43]
- contentinfo [ref=e44]:
- paragraph [ref=e45]: Version 14 Build 361
```
# Test source
```ts
1 | /**
2 | * Helpers pour les tests E2E avec FoundryVTT
3 | *
4 | * Fournit des fonctions utilitaires pour :
5 | * - Attendre que Foundry soit prêt
6 | * - Attendre que le module soit chargé
7 | * - Interagir avec l'UI FoundryVTT
8 | * - Gérer les sélecteurs spécifiques au module
9 | */
10 |
11 | import { expect } from '@playwright/test';
12 |
13 | /**
14 | * Attend que FoundryVTT soit complètement chargé
15 | * @param {import('@playwright/test').Page} page - La page Playwright
16 | * @param {number} timeout - Timeout en ms (défaut: 30000)
17 | */
18 | export async function waitForFoundryReady(page, timeout = 30000) {
> 19 | await page.waitForFunction(() => {
| ^ Error: page.waitForFunction: Test timeout of 120000ms exceeded.
20 | return typeof game !== 'undefined' && game.ready;
21 | }, { timeout });
22 | }
23 |
24 | /**
25 | * Attend que le module Video View Manager soit actif
26 | * @param {import('@playwright/test').Page} page - La page Playwright
27 | * @param {number} timeout - Timeout en ms (défaut: 15000)
28 | */
29 | export async function waitForVVMModule(page, timeout = 15000) {
30 | await page.waitForFunction(() => {
31 | const module = game.modules?.get?.('video-view-manager');
32 | return module?.active === true;
33 | }, { timeout });
34 | }
35 |
36 | /**
37 | * Attend qu'un élément du module soit présent
38 | * @param {import('@playwright/test').Page} page - La page Playwright
39 | * @param {string} selector - Sélecteur CSS
40 | * @param {number} timeout - Timeout en ms (défaut: 10000)
41 | */
42 | export async function waitForVVMElement(page, selector, timeout = 10000) {
43 | await page.waitForSelector(selector, {
44 | state: 'visible',
45 | timeout
46 | });
47 | }
48 |
49 | /**
50 | * Clique sur un bouton dans l'UI Foundry avec retry
51 | * @param {import('@playwright/test').Page} page - La page Playwright
52 | * @param {string|import('@playwright/test').Locator} button - Sélecteur ou Locator
53 | * @param {number} retries - Nombre de tentatives (défaut: 3)
54 | */
55 | export async function clickFoundryButton(page, button, retries = 3) {
56 | for (let i = 0; i < retries; i++) {
57 | try {
58 | const locator = typeof button === 'string' ? page.locator(button) : button;
59 | await locator.click({ timeout: 5000 });
60 | return;
61 | } catch (error) {
62 | if (i === retries - 1) throw error;
63 | await page.waitForTimeout(1000);
64 | }
65 | }
66 | }
67 |
68 | /**
69 | * Ouvre le sidebar de configuration Foundry
70 | * @param {import('@playwright/test').Page} page - La page Playwright
71 | */
72 | export async function openFoundrySidebar(page) {
73 | await clickFoundryButton(page, 'button[aria-label="Configure Settings"]');
74 | await page.waitForSelector('.app-v2.settings', { state: 'visible', timeout: 10000 });
75 | }
76 |
77 | /**
78 | * Ouvre le Director's Board (Epic 2)
79 | * @param {import('@playwright/test').Page} page - La page Playwright
80 | */
81 | export async function openDirectorsBoard(page) {
82 | // Le Director's Board a un bouton dédié dans la sidebar
83 | await page.waitForSelector('button[aria-label*="Director\'s Board"]', { timeout: 10000 });
84 | await clickFoundryButton(page, 'button[aria-label*="Director\'s Board"]');
85 |
86 | // Attendre que le board soit ouvert
87 | await page.waitForSelector('.scrying-pool-directors-board', {
88 | state: 'visible',
89 | timeout: 10000
90 | });
91 | }
92 |
93 | /**
94 | * Ouvre le Player Privacy Panel pour un utilisateur
95 | * @param {import('@playwright/test').Page} page - La page Playwright
96 | * @param {string} userId - L'ID de l'utilisateur
97 | */
98 | export async function openPlayerPrivacyPanel(page, userId) {
99 | // Le panel s'ouvre via les paramètres du module
100 | await openFoundrySidebar(page);
101 |
102 | // Naviguer vers les paramètres du module
103 | await clickFoundryButton(page, 'button:has-text("Video View Manager")');
104 | await page.waitForTimeout(1000);
105 |
106 | // Cliquer sur le bouton Player Privacy
107 | await clickFoundryButton(page, 'button:has-text("Player Privacy")');
108 |
109 | // Attendre le panel
110 | await page.waitForSelector('.sp-player-privacy-panel', {
111 | state: 'visible',
112 | timeout: 10000
113 | });
114 | }
115 |
116 | /**
117 | * Sélectionne un utilisateur dans une liste Foundry
118 | * @param {import('@playwright/test').Page} page - La page Playwright
119 | * @param {string} username - Le nom de l'utilisateur
```
@@ -0,0 +1,187 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: specs/epic-1-visibility.spec.js >> Epic 1: Core Camera Visibility Control >> FR-1: Hide/Show Participant via Context Menu >> Visibility change is immediate (no layout shift)
- Location: specs/epic-1-visibility.spec.js:92:5
# Error details
```
Test timeout of 120000ms exceeded while running "beforeEach" hook.
```
```
Error: page.waitForFunction: Test timeout of 120000ms exceeded.
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- list:
- listitem [ref=e2]:
- text:
- paragraph [ref=e3]: Foundry Virtual Tabletop nécessite une résolution d'écran de 1366px par 768px ou plus. Votre écran a une résolution de 1280px par 720px. Vous devez augmenter votre résolution ou utiliser un autre périphérique d'affichage, sinon certaines fonctionnalités du logiciel ne fonctionneront pas correctement.
- text:
- banner [ref=e5]:
- heading "Donjon & Cie" [level=1] [ref=e6]
- generic [ref=e7]:
- generic [ref=e8]:
- heading "Rejoindre la partie" [level=2] [ref=e9]
- combobox [ref=e14] [cursor=pointer]:
- option [selected]
- option "Gamemaster" [disabled]
- textbox "Mot de passe" [ref=e19]
- contentinfo [ref=e20]:
- button "Rejoindre la partie" [ref=e21] [cursor=pointer]:
- generic: Rejoindre la partie
- generic [ref=e22]:
- heading "Détails de la session" [level=2] [ref=e23]
- generic [ref=e25]: Prochaine partie
- generic [ref=e27]:
- generic [ref=e28]: Joueurs présents
- generic [ref=e30]:
- generic [ref=e31]: "1"
- generic [ref=e32]: /
- generic [ref=e33]: "1"
- generic [ref=e34]:
- heading "Retour à laccueil" [level=2] [ref=e35]
- textbox [ref=e39]
- contentinfo [ref=e40]:
- button "Retour à laccueil" [ref=e41] [cursor=pointer]:
- generic: Retour à laccueil
- article [ref=e42]:
- heading "Description du monde" [level=2] [ref=e43]
- contentinfo [ref=e44]:
- paragraph [ref=e45]: Version 14 Build 361
```
# Test source
```ts
1 | /**
2 | * Helpers pour les tests E2E avec FoundryVTT
3 | *
4 | * Fournit des fonctions utilitaires pour :
5 | * - Attendre que Foundry soit prêt
6 | * - Attendre que le module soit chargé
7 | * - Interagir avec l'UI FoundryVTT
8 | * - Gérer les sélecteurs spécifiques au module
9 | */
10 |
11 | import { expect } from '@playwright/test';
12 |
13 | /**
14 | * Attend que FoundryVTT soit complètement chargé
15 | * @param {import('@playwright/test').Page} page - La page Playwright
16 | * @param {number} timeout - Timeout en ms (défaut: 30000)
17 | */
18 | export async function waitForFoundryReady(page, timeout = 30000) {
> 19 | await page.waitForFunction(() => {
| ^ Error: page.waitForFunction: Test timeout of 120000ms exceeded.
20 | return typeof game !== 'undefined' && game.ready;
21 | }, { timeout });
22 | }
23 |
24 | /**
25 | * Attend que le module Video View Manager soit actif
26 | * @param {import('@playwright/test').Page} page - La page Playwright
27 | * @param {number} timeout - Timeout en ms (défaut: 15000)
28 | */
29 | export async function waitForVVMModule(page, timeout = 15000) {
30 | await page.waitForFunction(() => {
31 | const module = game.modules?.get?.('video-view-manager');
32 | return module?.active === true;
33 | }, { timeout });
34 | }
35 |
36 | /**
37 | * Attend qu'un élément du module soit présent
38 | * @param {import('@playwright/test').Page} page - La page Playwright
39 | * @param {string} selector - Sélecteur CSS
40 | * @param {number} timeout - Timeout en ms (défaut: 10000)
41 | */
42 | export async function waitForVVMElement(page, selector, timeout = 10000) {
43 | await page.waitForSelector(selector, {
44 | state: 'visible',
45 | timeout
46 | });
47 | }
48 |
49 | /**
50 | * Clique sur un bouton dans l'UI Foundry avec retry
51 | * @param {import('@playwright/test').Page} page - La page Playwright
52 | * @param {string|import('@playwright/test').Locator} button - Sélecteur ou Locator
53 | * @param {number} retries - Nombre de tentatives (défaut: 3)
54 | */
55 | export async function clickFoundryButton(page, button, retries = 3) {
56 | for (let i = 0; i < retries; i++) {
57 | try {
58 | const locator = typeof button === 'string' ? page.locator(button) : button;
59 | await locator.click({ timeout: 5000 });
60 | return;
61 | } catch (error) {
62 | if (i === retries - 1) throw error;
63 | await page.waitForTimeout(1000);
64 | }
65 | }
66 | }
67 |
68 | /**
69 | * Ouvre le sidebar de configuration Foundry
70 | * @param {import('@playwright/test').Page} page - La page Playwright
71 | */
72 | export async function openFoundrySidebar(page) {
73 | await clickFoundryButton(page, 'button[aria-label="Configure Settings"]');
74 | await page.waitForSelector('.app-v2.settings', { state: 'visible', timeout: 10000 });
75 | }
76 |
77 | /**
78 | * Ouvre le Director's Board (Epic 2)
79 | * @param {import('@playwright/test').Page} page - La page Playwright
80 | */
81 | export async function openDirectorsBoard(page) {
82 | // Le Director's Board a un bouton dédié dans la sidebar
83 | await page.waitForSelector('button[aria-label*="Director\'s Board"]', { timeout: 10000 });
84 | await clickFoundryButton(page, 'button[aria-label*="Director\'s Board"]');
85 |
86 | // Attendre que le board soit ouvert
87 | await page.waitForSelector('.scrying-pool-directors-board', {
88 | state: 'visible',
89 | timeout: 10000
90 | });
91 | }
92 |
93 | /**
94 | * Ouvre le Player Privacy Panel pour un utilisateur
95 | * @param {import('@playwright/test').Page} page - La page Playwright
96 | * @param {string} userId - L'ID de l'utilisateur
97 | */
98 | export async function openPlayerPrivacyPanel(page, userId) {
99 | // Le panel s'ouvre via les paramètres du module
100 | await openFoundrySidebar(page);
101 |
102 | // Naviguer vers les paramètres du module
103 | await clickFoundryButton(page, 'button:has-text("Video View Manager")');
104 | await page.waitForTimeout(1000);
105 |
106 | // Cliquer sur le bouton Player Privacy
107 | await clickFoundryButton(page, 'button:has-text("Player Privacy")');
108 |
109 | // Attendre le panel
110 | await page.waitForSelector('.sp-player-privacy-panel', {
111 | state: 'visible',
112 | timeout: 10000
113 | });
114 | }
115 |
116 | /**
117 | * Sélectionne un utilisateur dans une liste Foundry
118 | * @param {import('@playwright/test').Page} page - La page Playwright
119 | * @param {string} username - Le nom de l'utilisateur
```
@@ -0,0 +1,187 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: specs/epic-1-visibility.spec.js >> Epic 1: Core Camera Visibility Control >> FR-1: Hide/Show Participant via Context Menu >> GM can show a hidden participant via right-click
- Location: specs/epic-1-visibility.spec.js:76:5
# Error details
```
Test timeout of 120000ms exceeded while running "beforeEach" hook.
```
```
Error: page.waitForFunction: Test timeout of 120000ms exceeded.
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- list:
- listitem [ref=e2]:
- text:
- paragraph [ref=e3]: Foundry Virtual Tabletop nécessite une résolution d'écran de 1366px par 768px ou plus. Votre écran a une résolution de 1280px par 720px. Vous devez augmenter votre résolution ou utiliser un autre périphérique d'affichage, sinon certaines fonctionnalités du logiciel ne fonctionneront pas correctement.
- text:
- banner [ref=e5]:
- heading "Donjon & Cie" [level=1] [ref=e6]
- generic [ref=e7]:
- generic [ref=e8]:
- heading "Rejoindre la partie" [level=2] [ref=e9]
- combobox [ref=e14] [cursor=pointer]:
- option [selected]
- option "Gamemaster" [disabled]
- textbox "Mot de passe" [ref=e19]
- contentinfo [ref=e20]:
- button "Rejoindre la partie" [ref=e21] [cursor=pointer]:
- generic: Rejoindre la partie
- generic [ref=e22]:
- heading "Détails de la session" [level=2] [ref=e23]
- generic [ref=e25]: Prochaine partie
- generic [ref=e27]:
- generic [ref=e28]: Joueurs présents
- generic [ref=e30]:
- generic [ref=e31]: "1"
- generic [ref=e32]: /
- generic [ref=e33]: "1"
- generic [ref=e34]:
- heading "Retour à laccueil" [level=2] [ref=e35]
- textbox [ref=e39]
- contentinfo [ref=e40]:
- button "Retour à laccueil" [ref=e41] [cursor=pointer]:
- generic: Retour à laccueil
- article [ref=e42]:
- heading "Description du monde" [level=2] [ref=e43]
- contentinfo [ref=e44]:
- paragraph [ref=e45]: Version 14 Build 361
```
# Test source
```ts
1 | /**
2 | * Helpers pour les tests E2E avec FoundryVTT
3 | *
4 | * Fournit des fonctions utilitaires pour :
5 | * - Attendre que Foundry soit prêt
6 | * - Attendre que le module soit chargé
7 | * - Interagir avec l'UI FoundryVTT
8 | * - Gérer les sélecteurs spécifiques au module
9 | */
10 |
11 | import { expect } from '@playwright/test';
12 |
13 | /**
14 | * Attend que FoundryVTT soit complètement chargé
15 | * @param {import('@playwright/test').Page} page - La page Playwright
16 | * @param {number} timeout - Timeout en ms (défaut: 30000)
17 | */
18 | export async function waitForFoundryReady(page, timeout = 30000) {
> 19 | await page.waitForFunction(() => {
| ^ Error: page.waitForFunction: Test timeout of 120000ms exceeded.
20 | return typeof game !== 'undefined' && game.ready;
21 | }, { timeout });
22 | }
23 |
24 | /**
25 | * Attend que le module Video View Manager soit actif
26 | * @param {import('@playwright/test').Page} page - La page Playwright
27 | * @param {number} timeout - Timeout en ms (défaut: 15000)
28 | */
29 | export async function waitForVVMModule(page, timeout = 15000) {
30 | await page.waitForFunction(() => {
31 | const module = game.modules?.get?.('video-view-manager');
32 | return module?.active === true;
33 | }, { timeout });
34 | }
35 |
36 | /**
37 | * Attend qu'un élément du module soit présent
38 | * @param {import('@playwright/test').Page} page - La page Playwright
39 | * @param {string} selector - Sélecteur CSS
40 | * @param {number} timeout - Timeout en ms (défaut: 10000)
41 | */
42 | export async function waitForVVMElement(page, selector, timeout = 10000) {
43 | await page.waitForSelector(selector, {
44 | state: 'visible',
45 | timeout
46 | });
47 | }
48 |
49 | /**
50 | * Clique sur un bouton dans l'UI Foundry avec retry
51 | * @param {import('@playwright/test').Page} page - La page Playwright
52 | * @param {string|import('@playwright/test').Locator} button - Sélecteur ou Locator
53 | * @param {number} retries - Nombre de tentatives (défaut: 3)
54 | */
55 | export async function clickFoundryButton(page, button, retries = 3) {
56 | for (let i = 0; i < retries; i++) {
57 | try {
58 | const locator = typeof button === 'string' ? page.locator(button) : button;
59 | await locator.click({ timeout: 5000 });
60 | return;
61 | } catch (error) {
62 | if (i === retries - 1) throw error;
63 | await page.waitForTimeout(1000);
64 | }
65 | }
66 | }
67 |
68 | /**
69 | * Ouvre le sidebar de configuration Foundry
70 | * @param {import('@playwright/test').Page} page - La page Playwright
71 | */
72 | export async function openFoundrySidebar(page) {
73 | await clickFoundryButton(page, 'button[aria-label="Configure Settings"]');
74 | await page.waitForSelector('.app-v2.settings', { state: 'visible', timeout: 10000 });
75 | }
76 |
77 | /**
78 | * Ouvre le Director's Board (Epic 2)
79 | * @param {import('@playwright/test').Page} page - La page Playwright
80 | */
81 | export async function openDirectorsBoard(page) {
82 | // Le Director's Board a un bouton dédié dans la sidebar
83 | await page.waitForSelector('button[aria-label*="Director\'s Board"]', { timeout: 10000 });
84 | await clickFoundryButton(page, 'button[aria-label*="Director\'s Board"]');
85 |
86 | // Attendre que le board soit ouvert
87 | await page.waitForSelector('.scrying-pool-directors-board', {
88 | state: 'visible',
89 | timeout: 10000
90 | });
91 | }
92 |
93 | /**
94 | * Ouvre le Player Privacy Panel pour un utilisateur
95 | * @param {import('@playwright/test').Page} page - La page Playwright
96 | * @param {string} userId - L'ID de l'utilisateur
97 | */
98 | export async function openPlayerPrivacyPanel(page, userId) {
99 | // Le panel s'ouvre via les paramètres du module
100 | await openFoundrySidebar(page);
101 |
102 | // Naviguer vers les paramètres du module
103 | await clickFoundryButton(page, 'button:has-text("Video View Manager")');
104 | await page.waitForTimeout(1000);
105 |
106 | // Cliquer sur le bouton Player Privacy
107 | await clickFoundryButton(page, 'button:has-text("Player Privacy")');
108 |
109 | // Attendre le panel
110 | await page.waitForSelector('.sp-player-privacy-panel', {
111 | state: 'visible',
112 | timeout: 10000
113 | });
114 | }
115 |
116 | /**
117 | * Sélectionne un utilisateur dans une liste Foundry
118 | * @param {import('@playwright/test').Page} page - La page Playwright
119 | * @param {string} username - Le nom de l'utilisateur
```
@@ -0,0 +1,187 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: specs/epic-1-visibility.spec.js >> Epic 1: Core Camera Visibility Control >> FR-1: Hide/Show Participant via Context Menu >> GM can hide a participant from the table via right-click
- Location: specs/epic-1-visibility.spec.js:60:5
# Error details
```
Test timeout of 120000ms exceeded while running "beforeEach" hook.
```
```
Error: page.waitForFunction: Test timeout of 120000ms exceeded.
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- list:
- listitem [ref=e2]:
- text:
- paragraph [ref=e3]: Foundry Virtual Tabletop nécessite une résolution d'écran de 1366px par 768px ou plus. Votre écran a une résolution de 1280px par 720px. Vous devez augmenter votre résolution ou utiliser un autre périphérique d'affichage, sinon certaines fonctionnalités du logiciel ne fonctionneront pas correctement.
- text:
- banner [ref=e5]:
- heading "Donjon & Cie" [level=1] [ref=e6]
- generic [ref=e7]:
- generic [ref=e8]:
- heading "Rejoindre la partie" [level=2] [ref=e9]
- combobox [ref=e14] [cursor=pointer]:
- option [selected]
- option "Gamemaster" [disabled]
- textbox "Mot de passe" [ref=e19]
- contentinfo [ref=e20]:
- button "Rejoindre la partie" [ref=e21] [cursor=pointer]:
- generic: Rejoindre la partie
- generic [ref=e22]:
- heading "Détails de la session" [level=2] [ref=e23]
- generic [ref=e25]: Prochaine partie
- generic [ref=e27]:
- generic [ref=e28]: Joueurs présents
- generic [ref=e30]:
- generic [ref=e31]: "1"
- generic [ref=e32]: /
- generic [ref=e33]: "1"
- generic [ref=e34]:
- heading "Retour à laccueil" [level=2] [ref=e35]
- textbox [ref=e39]
- contentinfo [ref=e40]:
- button "Retour à laccueil" [ref=e41] [cursor=pointer]:
- generic: Retour à laccueil
- article [ref=e42]:
- heading "Description du monde" [level=2] [ref=e43]
- contentinfo [ref=e44]:
- paragraph [ref=e45]: Version 14 Build 361
```
# Test source
```ts
1 | /**
2 | * Helpers pour les tests E2E avec FoundryVTT
3 | *
4 | * Fournit des fonctions utilitaires pour :
5 | * - Attendre que Foundry soit prêt
6 | * - Attendre que le module soit chargé
7 | * - Interagir avec l'UI FoundryVTT
8 | * - Gérer les sélecteurs spécifiques au module
9 | */
10 |
11 | import { expect } from '@playwright/test';
12 |
13 | /**
14 | * Attend que FoundryVTT soit complètement chargé
15 | * @param {import('@playwright/test').Page} page - La page Playwright
16 | * @param {number} timeout - Timeout en ms (défaut: 30000)
17 | */
18 | export async function waitForFoundryReady(page, timeout = 30000) {
> 19 | await page.waitForFunction(() => {
| ^ Error: page.waitForFunction: Test timeout of 120000ms exceeded.
20 | return typeof game !== 'undefined' && game.ready;
21 | }, { timeout });
22 | }
23 |
24 | /**
25 | * Attend que le module Video View Manager soit actif
26 | * @param {import('@playwright/test').Page} page - La page Playwright
27 | * @param {number} timeout - Timeout en ms (défaut: 15000)
28 | */
29 | export async function waitForVVMModule(page, timeout = 15000) {
30 | await page.waitForFunction(() => {
31 | const module = game.modules?.get?.('video-view-manager');
32 | return module?.active === true;
33 | }, { timeout });
34 | }
35 |
36 | /**
37 | * Attend qu'un élément du module soit présent
38 | * @param {import('@playwright/test').Page} page - La page Playwright
39 | * @param {string} selector - Sélecteur CSS
40 | * @param {number} timeout - Timeout en ms (défaut: 10000)
41 | */
42 | export async function waitForVVMElement(page, selector, timeout = 10000) {
43 | await page.waitForSelector(selector, {
44 | state: 'visible',
45 | timeout
46 | });
47 | }
48 |
49 | /**
50 | * Clique sur un bouton dans l'UI Foundry avec retry
51 | * @param {import('@playwright/test').Page} page - La page Playwright
52 | * @param {string|import('@playwright/test').Locator} button - Sélecteur ou Locator
53 | * @param {number} retries - Nombre de tentatives (défaut: 3)
54 | */
55 | export async function clickFoundryButton(page, button, retries = 3) {
56 | for (let i = 0; i < retries; i++) {
57 | try {
58 | const locator = typeof button === 'string' ? page.locator(button) : button;
59 | await locator.click({ timeout: 5000 });
60 | return;
61 | } catch (error) {
62 | if (i === retries - 1) throw error;
63 | await page.waitForTimeout(1000);
64 | }
65 | }
66 | }
67 |
68 | /**
69 | * Ouvre le sidebar de configuration Foundry
70 | * @param {import('@playwright/test').Page} page - La page Playwright
71 | */
72 | export async function openFoundrySidebar(page) {
73 | await clickFoundryButton(page, 'button[aria-label="Configure Settings"]');
74 | await page.waitForSelector('.app-v2.settings', { state: 'visible', timeout: 10000 });
75 | }
76 |
77 | /**
78 | * Ouvre le Director's Board (Epic 2)
79 | * @param {import('@playwright/test').Page} page - La page Playwright
80 | */
81 | export async function openDirectorsBoard(page) {
82 | // Le Director's Board a un bouton dédié dans la sidebar
83 | await page.waitForSelector('button[aria-label*="Director\'s Board"]', { timeout: 10000 });
84 | await clickFoundryButton(page, 'button[aria-label*="Director\'s Board"]');
85 |
86 | // Attendre que le board soit ouvert
87 | await page.waitForSelector('.scrying-pool-directors-board', {
88 | state: 'visible',
89 | timeout: 10000
90 | });
91 | }
92 |
93 | /**
94 | * Ouvre le Player Privacy Panel pour un utilisateur
95 | * @param {import('@playwright/test').Page} page - La page Playwright
96 | * @param {string} userId - L'ID de l'utilisateur
97 | */
98 | export async function openPlayerPrivacyPanel(page, userId) {
99 | // Le panel s'ouvre via les paramètres du module
100 | await openFoundrySidebar(page);
101 |
102 | // Naviguer vers les paramètres du module
103 | await clickFoundryButton(page, 'button:has-text("Video View Manager")');
104 | await page.waitForTimeout(1000);
105 |
106 | // Cliquer sur le bouton Player Privacy
107 | await clickFoundryButton(page, 'button:has-text("Player Privacy")');
108 |
109 | // Attendre le panel
110 | await page.waitForSelector('.sp-player-privacy-panel', {
111 | state: 'visible',
112 | timeout: 10000
113 | });
114 | }
115 |
116 | /**
117 | * Sélectionne un utilisateur dans une liste Foundry
118 | * @param {import('@playwright/test').Page} page - La page Playwright
119 | * @param {string} username - Le nom de l'utilisateur
```
@@ -0,0 +1,187 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: specs/epic-1-visibility.spec.js >> Epic 1: Core Camera Visibility Control >> FR-4: AV Tile Visual State Indicators >> Hidden state shows grey overlay + lock icon + tooltip
- Location: specs/epic-1-visibility.spec.js:115:5
# Error details
```
Test timeout of 120000ms exceeded while running "beforeEach" hook.
```
```
Error: page.waitForFunction: Test timeout of 120000ms exceeded.
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- list:
- listitem [ref=e2]:
- text:
- paragraph [ref=e3]: Foundry Virtual Tabletop nécessite une résolution d'écran de 1366px par 768px ou plus. Votre écran a une résolution de 1280px par 720px. Vous devez augmenter votre résolution ou utiliser un autre périphérique d'affichage, sinon certaines fonctionnalités du logiciel ne fonctionneront pas correctement.
- text:
- banner [ref=e5]:
- heading "Donjon & Cie" [level=1] [ref=e6]
- generic [ref=e7]:
- generic [ref=e8]:
- heading "Rejoindre la partie" [level=2] [ref=e9]
- combobox [ref=e14] [cursor=pointer]:
- option [selected]
- option "Gamemaster" [disabled]
- textbox "Mot de passe" [ref=e19]
- contentinfo [ref=e20]:
- button "Rejoindre la partie" [ref=e21] [cursor=pointer]:
- generic: Rejoindre la partie
- generic [ref=e22]:
- heading "Détails de la session" [level=2] [ref=e23]
- generic [ref=e25]: Prochaine partie
- generic [ref=e27]:
- generic [ref=e28]: Joueurs présents
- generic [ref=e30]:
- generic [ref=e31]: "1"
- generic [ref=e32]: /
- generic [ref=e33]: "1"
- generic [ref=e34]:
- heading "Retour à laccueil" [level=2] [ref=e35]
- textbox [ref=e39]
- contentinfo [ref=e40]:
- button "Retour à laccueil" [ref=e41] [cursor=pointer]:
- generic: Retour à laccueil
- article [ref=e42]:
- heading "Description du monde" [level=2] [ref=e43]
- contentinfo [ref=e44]:
- paragraph [ref=e45]: Version 14 Build 361
```
# Test source
```ts
1 | /**
2 | * Helpers pour les tests E2E avec FoundryVTT
3 | *
4 | * Fournit des fonctions utilitaires pour :
5 | * - Attendre que Foundry soit prêt
6 | * - Attendre que le module soit chargé
7 | * - Interagir avec l'UI FoundryVTT
8 | * - Gérer les sélecteurs spécifiques au module
9 | */
10 |
11 | import { expect } from '@playwright/test';
12 |
13 | /**
14 | * Attend que FoundryVTT soit complètement chargé
15 | * @param {import('@playwright/test').Page} page - La page Playwright
16 | * @param {number} timeout - Timeout en ms (défaut: 30000)
17 | */
18 | export async function waitForFoundryReady(page, timeout = 30000) {
> 19 | await page.waitForFunction(() => {
| ^ Error: page.waitForFunction: Test timeout of 120000ms exceeded.
20 | return typeof game !== 'undefined' && game.ready;
21 | }, { timeout });
22 | }
23 |
24 | /**
25 | * Attend que le module Video View Manager soit actif
26 | * @param {import('@playwright/test').Page} page - La page Playwright
27 | * @param {number} timeout - Timeout en ms (défaut: 15000)
28 | */
29 | export async function waitForVVMModule(page, timeout = 15000) {
30 | await page.waitForFunction(() => {
31 | const module = game.modules?.get?.('video-view-manager');
32 | return module?.active === true;
33 | }, { timeout });
34 | }
35 |
36 | /**
37 | * Attend qu'un élément du module soit présent
38 | * @param {import('@playwright/test').Page} page - La page Playwright
39 | * @param {string} selector - Sélecteur CSS
40 | * @param {number} timeout - Timeout en ms (défaut: 10000)
41 | */
42 | export async function waitForVVMElement(page, selector, timeout = 10000) {
43 | await page.waitForSelector(selector, {
44 | state: 'visible',
45 | timeout
46 | });
47 | }
48 |
49 | /**
50 | * Clique sur un bouton dans l'UI Foundry avec retry
51 | * @param {import('@playwright/test').Page} page - La page Playwright
52 | * @param {string|import('@playwright/test').Locator} button - Sélecteur ou Locator
53 | * @param {number} retries - Nombre de tentatives (défaut: 3)
54 | */
55 | export async function clickFoundryButton(page, button, retries = 3) {
56 | for (let i = 0; i < retries; i++) {
57 | try {
58 | const locator = typeof button === 'string' ? page.locator(button) : button;
59 | await locator.click({ timeout: 5000 });
60 | return;
61 | } catch (error) {
62 | if (i === retries - 1) throw error;
63 | await page.waitForTimeout(1000);
64 | }
65 | }
66 | }
67 |
68 | /**
69 | * Ouvre le sidebar de configuration Foundry
70 | * @param {import('@playwright/test').Page} page - La page Playwright
71 | */
72 | export async function openFoundrySidebar(page) {
73 | await clickFoundryButton(page, 'button[aria-label="Configure Settings"]');
74 | await page.waitForSelector('.app-v2.settings', { state: 'visible', timeout: 10000 });
75 | }
76 |
77 | /**
78 | * Ouvre le Director's Board (Epic 2)
79 | * @param {import('@playwright/test').Page} page - La page Playwright
80 | */
81 | export async function openDirectorsBoard(page) {
82 | // Le Director's Board a un bouton dédié dans la sidebar
83 | await page.waitForSelector('button[aria-label*="Director\'s Board"]', { timeout: 10000 });
84 | await clickFoundryButton(page, 'button[aria-label*="Director\'s Board"]');
85 |
86 | // Attendre que le board soit ouvert
87 | await page.waitForSelector('.scrying-pool-directors-board', {
88 | state: 'visible',
89 | timeout: 10000
90 | });
91 | }
92 |
93 | /**
94 | * Ouvre le Player Privacy Panel pour un utilisateur
95 | * @param {import('@playwright/test').Page} page - La page Playwright
96 | * @param {string} userId - L'ID de l'utilisateur
97 | */
98 | export async function openPlayerPrivacyPanel(page, userId) {
99 | // Le panel s'ouvre via les paramètres du module
100 | await openFoundrySidebar(page);
101 |
102 | // Naviguer vers les paramètres du module
103 | await clickFoundryButton(page, 'button:has-text("Video View Manager")');
104 | await page.waitForTimeout(1000);
105 |
106 | // Cliquer sur le bouton Player Privacy
107 | await clickFoundryButton(page, 'button:has-text("Player Privacy")');
108 |
109 | // Attendre le panel
110 | await page.waitForSelector('.sp-player-privacy-panel', {
111 | state: 'visible',
112 | timeout: 10000
113 | });
114 | }
115 |
116 | /**
117 | * Sélectionne un utilisateur dans une liste Foundry
118 | * @param {import('@playwright/test').Page} page - La page Playwright
119 | * @param {string} username - Le nom de l'utilisateur
```
@@ -0,0 +1,141 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: specs/epic-1-visibility.spec.js >> Epic 1: Core Camera Visibility Control >> FR-4: AV Tile Visual State Indicators >> Self-muted state shows camera-off icon
- Location: specs/epic-1-visibility.spec.js:132:5
# Error details
```
Error: page.waitForFunction: Test ended.
```
# Test source
```ts
1 | /**
2 | * Helpers pour les tests E2E avec FoundryVTT
3 | *
4 | * Fournit des fonctions utilitaires pour :
5 | * - Attendre que Foundry soit prêt
6 | * - Attendre que le module soit chargé
7 | * - Interagir avec l'UI FoundryVTT
8 | * - Gérer les sélecteurs spécifiques au module
9 | */
10 |
11 | import { expect } from '@playwright/test';
12 |
13 | /**
14 | * Attend que FoundryVTT soit complètement chargé
15 | * @param {import('@playwright/test').Page} page - La page Playwright
16 | * @param {number} timeout - Timeout en ms (défaut: 30000)
17 | */
18 | export async function waitForFoundryReady(page, timeout = 30000) {
> 19 | await page.waitForFunction(() => {
| ^ Error: page.waitForFunction: Test ended.
20 | return typeof game !== 'undefined' && game.ready;
21 | }, { timeout });
22 | }
23 |
24 | /**
25 | * Attend que le module Video View Manager soit actif
26 | * @param {import('@playwright/test').Page} page - La page Playwright
27 | * @param {number} timeout - Timeout en ms (défaut: 15000)
28 | */
29 | export async function waitForVVMModule(page, timeout = 15000) {
30 | await page.waitForFunction(() => {
31 | const module = game.modules?.get?.('video-view-manager');
32 | return module?.active === true;
33 | }, { timeout });
34 | }
35 |
36 | /**
37 | * Attend qu'un élément du module soit présent
38 | * @param {import('@playwright/test').Page} page - La page Playwright
39 | * @param {string} selector - Sélecteur CSS
40 | * @param {number} timeout - Timeout en ms (défaut: 10000)
41 | */
42 | export async function waitForVVMElement(page, selector, timeout = 10000) {
43 | await page.waitForSelector(selector, {
44 | state: 'visible',
45 | timeout
46 | });
47 | }
48 |
49 | /**
50 | * Clique sur un bouton dans l'UI Foundry avec retry
51 | * @param {import('@playwright/test').Page} page - La page Playwright
52 | * @param {string|import('@playwright/test').Locator} button - Sélecteur ou Locator
53 | * @param {number} retries - Nombre de tentatives (défaut: 3)
54 | */
55 | export async function clickFoundryButton(page, button, retries = 3) {
56 | for (let i = 0; i < retries; i++) {
57 | try {
58 | const locator = typeof button === 'string' ? page.locator(button) : button;
59 | await locator.click({ timeout: 5000 });
60 | return;
61 | } catch (error) {
62 | if (i === retries - 1) throw error;
63 | await page.waitForTimeout(1000);
64 | }
65 | }
66 | }
67 |
68 | /**
69 | * Ouvre le sidebar de configuration Foundry
70 | * @param {import('@playwright/test').Page} page - La page Playwright
71 | */
72 | export async function openFoundrySidebar(page) {
73 | await clickFoundryButton(page, 'button[aria-label="Configure Settings"]');
74 | await page.waitForSelector('.app-v2.settings', { state: 'visible', timeout: 10000 });
75 | }
76 |
77 | /**
78 | * Ouvre le Director's Board (Epic 2)
79 | * @param {import('@playwright/test').Page} page - La page Playwright
80 | */
81 | export async function openDirectorsBoard(page) {
82 | // Le Director's Board a un bouton dédié dans la sidebar
83 | await page.waitForSelector('button[aria-label*="Director\'s Board"]', { timeout: 10000 });
84 | await clickFoundryButton(page, 'button[aria-label*="Director\'s Board"]');
85 |
86 | // Attendre que le board soit ouvert
87 | await page.waitForSelector('.scrying-pool-directors-board', {
88 | state: 'visible',
89 | timeout: 10000
90 | });
91 | }
92 |
93 | /**
94 | * Ouvre le Player Privacy Panel pour un utilisateur
95 | * @param {import('@playwright/test').Page} page - La page Playwright
96 | * @param {string} userId - L'ID de l'utilisateur
97 | */
98 | export async function openPlayerPrivacyPanel(page, userId) {
99 | // Le panel s'ouvre via les paramètres du module
100 | await openFoundrySidebar(page);
101 |
102 | // Naviguer vers les paramètres du module
103 | await clickFoundryButton(page, 'button:has-text("Video View Manager")');
104 | await page.waitForTimeout(1000);
105 |
106 | // Cliquer sur le bouton Player Privacy
107 | await clickFoundryButton(page, 'button:has-text("Player Privacy")');
108 |
109 | // Attendre le panel
110 | await page.waitForSelector('.sp-player-privacy-panel', {
111 | state: 'visible',
112 | timeout: 10000
113 | });
114 | }
115 |
116 | /**
117 | * Sélectionne un utilisateur dans une liste Foundry
118 | * @param {import('@playwright/test').Page} page - La page Playwright
119 | * @param {string} username - Le nom de l'utilisateur
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
(function(){const r=document.createElement("link").relList;if(r&&r.supports&&r.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))s(e);new MutationObserver(e=>{for(const t of e)if(t.type==="childList")for(const o of t.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&s(o)}).observe(document,{childList:!0,subtree:!0});function n(e){const t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin==="use-credentials"?t.credentials="include":e.crossOrigin==="anonymous"?t.credentials="omit":t.credentials="same-origin",t}function s(e){if(e.ep)return;e.ep=!0;const t=n(e);fetch(e.href,t)}})();function c(i,r){try{return["http:","https:"].includes(new URL(i,r).protocol)}catch{return!1}}export{c as i};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
.drop-target{display:flex;align-items:center;justify-content:center;flex:auto;flex-direction:column;background-color:var(--vscode-editor-background);position:absolute;top:0;right:0;bottom:0;left:0;z-index:100;line-height:24px}body .drop-target{background:#fffc}:root.dark-mode .drop-target{background:#000c}.drop-target .title{font-size:24px;font-weight:700;margin-bottom:30px}.drop-target .info{max-width:400px;text-align:center}.drop-target .processing-error{font-size:24px;color:#e74c3c;font-weight:700;text-align:center;margin:30px;white-space:pre-line}.drop-target input{margin-top:50px}.drop-target button{color:#fff;background-color:#007acc;padding:8px 12px;border:none;margin:30px 0;cursor:pointer}.drop-target .version{color:var(--vscode-disabledForeground);margin-top:8px}.progress-dialog{width:400px;top:0;right:0;bottom:0;left:0;border:none;outline:none;background-color:var(--vscode-sideBar-background)}.progress-dialog::backdrop{background-color:#0006}.progress-content{padding:16px}.progress-content .title{background-color:unset;font-size:18px;font-weight:700;padding:0}.progress-wrapper{background-color:var(--vscode-commandCenter-activeBackground);width:100%;margin-top:16px;margin-bottom:8px}.inner-progress{background-color:var(--vscode-progressBar-background);height:4px}.workbench-loader-header{display:flex;background-color:var(--vscode-sideBar-background);flex:none;flex-basis:48px;line-height:48px;font-size:16px;align-items:center;box-shadow:var(--vscode-scrollbar-shadow) 0 6px 6px -6px}.workbench-loader{contain:size}.workbench-loader .workbench-loader-header{flex-basis:32px;line-height:32px;font-size:13px}.workbench-loader .workbench-loader-header .toolbar-button{margin:4px}.workbench-loader .logo{margin-left:16px;display:flex;align-items:center}.workbench-loader .logo img{height:32px;width:32px;pointer-events:none;flex:none}.workbench-loader .product{font-weight:600;margin-left:16px;flex:none}.workbench-loader .workbench-loader-header .title{margin-left:16px;overflow:hidden;text-overflow:ellipsis;text-wrap:nowrap}html,body{min-width:550px;min-height:450px;overflow:auto}
@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="en" translate="no">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="./playwright-logo.svg" type="image/svg+xml">
<link rel="manifest" href="./manifest.webmanifest">
<title>Playwright Trace Viewer</title>
<script type="module" crossorigin src="./index.BCnMPevh.js"></script>
<link rel="modulepreload" crossorigin href="./assets/urlMatch-BYQrIQwR.js">
<link rel="modulepreload" crossorigin href="./assets/defaultSettingsView-D31xz8zv.js">
<link rel="stylesheet" crossorigin href="./defaultSettingsView.BDKsFU3c.css">
<link rel="stylesheet" crossorigin href="./index.CzXZzn5A.css">
</head>
<body>
<div id="root"></div>
<dialog id="fallback-error">
<p>The Playwright Trace Viewer must be loaded over the <code>http://</code> or <code>https://</code> protocols.</p>
<p>For more information, please see the <a href="https://aka.ms/playwright/trace-viewer-file-protocol">docs</a>.</p>
</dialog>
<script>
if (!/^https?:/.test(window.location.protocol)) {
const fallbackErrorDialog = document.getElementById('fallback-error');
const isTraceViewerInsidePlaywrightReport = window.location.protocol === 'file:' && window.location.pathname.endsWith('/trace/index.html');
// Best-effort to show the report path in the dialog.
if (isTraceViewerInsidePlaywrightReport) {
const reportPath = (() => {
const base = decodeURIComponent(window.location.pathname).replace(/\/trace\/index\.html$/, '');
if (navigator.platform === 'Win32')
return base.replace(/^\//, '').replace(/\//g, '\\\\');
return base;
})();
const reportLink = document.createElement('div');
const command = `npx playwright show-report "${reportPath}"`;
reportLink.innerHTML = `You can open the report via <code>${command}</code> from your Playwright project. <button type="button">Copy Command</button>`;
fallbackErrorDialog.insertBefore(reportLink, fallbackErrorDialog.children[1]);
reportLink.querySelector('button').addEventListener('click', () => navigator.clipboard.writeText(command));
}
fallbackErrorDialog.show();
}
</script>
</body>
</html>
@@ -0,0 +1,16 @@
{
"theme_color": "#000",
"background_color": "#fff",
"display": "standalone",
"start_url": "index.html",
"name": "Playwright Trace Viewer",
"short_name": "Trace Viewer",
"icons": [
{
"src": "playwright-logo.svg",
"sizes": "48x48 72x72 96x96 128x128 150x150 256x256 512x512 1024x1024",
"type": "image/svg+xml",
"purpose": "any"
}
]
}
@@ -0,0 +1,9 @@
<svg width="400" height="400" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M136.444 221.556C123.558 225.213 115.104 231.625 109.535 238.032C114.869 233.364 122.014 229.08 131.652 226.348C141.51 223.554 149.92 223.574 156.869 224.915V219.481C150.941 218.939 144.145 219.371 136.444 221.556ZM108.946 175.876L61.0895 188.484C61.0895 188.484 61.9617 189.716 63.5767 191.36L104.153 180.668C104.153 180.668 103.578 188.077 98.5847 194.705C108.03 187.559 108.946 175.876 108.946 175.876ZM149.005 288.347C81.6582 306.486 46.0272 228.438 35.2396 187.928C30.2556 169.229 28.0799 155.067 27.5 145.928C27.4377 144.979 27.4665 144.179 27.5336 143.446C24.04 143.657 22.3674 145.473 22.7077 150.721C23.2876 159.855 25.4633 174.016 30.4473 192.721C41.2301 233.225 76.8659 311.273 144.213 293.134C158.872 289.185 169.885 281.992 178.152 272.81C170.532 279.692 160.995 285.112 149.005 288.347ZM161.661 128.11V132.903H188.077C187.535 131.206 186.989 129.677 186.447 128.11H161.661Z" fill="#2D4552"/>
<path d="M193.981 167.584C205.861 170.958 212.144 179.287 215.465 186.658L228.711 190.42C228.711 190.42 226.904 164.623 203.57 157.995C181.741 151.793 168.308 170.124 166.674 172.496C173.024 167.972 182.297 164.268 193.981 167.584ZM299.422 186.777C277.573 180.547 264.145 198.916 262.535 201.255C268.89 196.736 278.158 193.031 289.837 196.362C301.698 199.741 307.976 208.06 311.307 215.436L324.572 219.212C324.572 219.212 322.736 193.41 299.422 186.777ZM286.262 254.795L176.072 223.99C176.072 223.99 177.265 230.038 181.842 237.869L274.617 263.805C282.255 259.386 286.262 254.795 286.262 254.795ZM209.867 321.102C122.618 297.71 133.166 186.543 147.284 133.865C153.097 112.156 159.073 96.0203 164.029 85.204C161.072 84.5953 158.623 86.1529 156.203 91.0746C150.941 101.747 144.212 119.124 137.7 143.45C123.586 196.127 113.038 307.29 200.283 330.682C241.406 341.699 273.442 324.955 297.323 298.659C274.655 319.19 245.714 330.701 209.867 321.102Z" fill="#2D4552"/>
<path d="M161.661 262.296V239.863L99.3324 257.537C99.3324 257.537 103.938 230.777 136.444 221.556C146.302 218.762 154.713 218.781 161.661 220.123V128.11H192.869C189.471 117.61 186.184 109.526 183.423 103.909C178.856 94.612 174.174 100.775 163.545 109.665C156.059 115.919 137.139 129.261 108.668 136.933C80.1966 144.61 57.179 142.574 47.5752 140.911C33.9601 138.562 26.8387 135.572 27.5049 145.928C28.0847 155.062 30.2605 169.224 35.2445 187.928C46.0272 228.433 81.663 306.481 149.01 288.342C166.602 283.602 179.019 274.233 187.626 262.291H161.661V262.296ZM61.0848 188.484L108.946 175.876C108.946 175.876 107.551 194.288 89.6087 199.018C71.6614 203.743 61.0848 188.484 61.0848 188.484Z" fill="#E2574C"/>
<path d="M341.786 129.174C329.345 131.355 299.498 134.072 262.612 124.185C225.716 114.304 201.236 97.0224 191.537 88.8994C177.788 77.3834 171.74 69.3802 165.788 81.4857C160.526 92.163 153.797 109.54 147.284 133.866C133.171 186.543 122.623 297.706 209.867 321.098C297.093 344.47 343.53 242.92 357.644 190.238C364.157 165.917 367.013 147.5 367.799 135.625C368.695 122.173 359.455 126.078 341.786 129.174ZM166.497 172.756C166.497 172.756 180.246 151.372 203.565 158C226.899 164.628 228.706 190.425 228.706 190.425L166.497 172.756ZM223.42 268.713C182.403 256.698 176.077 223.99 176.077 223.99L286.262 254.796C286.262 254.791 264.021 280.578 223.42 268.713ZM262.377 201.495C262.377 201.495 276.107 180.126 299.422 186.773C322.736 193.411 324.572 219.208 324.572 219.208L262.377 201.495Z" fill="#2EAD33"/>
<path d="M139.88 246.04L99.3324 257.532C99.3324 257.532 103.737 232.44 133.607 222.496L110.647 136.33L108.663 136.933C80.1918 144.611 57.1742 142.574 47.5704 140.911C33.9554 138.563 26.834 135.572 27.5001 145.929C28.08 155.063 30.2557 169.224 35.2397 187.929C46.0225 228.433 81.6583 306.481 149.005 288.342L150.989 287.719L139.88 246.04ZM61.0848 188.485L108.946 175.876C108.946 175.876 107.551 194.288 89.6087 199.018C71.6615 203.743 61.0848 188.485 61.0848 188.485Z" fill="#D65348"/>
<path d="M225.27 269.163L223.415 268.712C182.398 256.698 176.072 223.99 176.072 223.99L232.89 239.872L262.971 124.281L262.607 124.185C225.711 114.304 201.232 97.0224 191.532 88.8994C177.783 77.3834 171.735 69.3802 165.783 81.4857C160.526 92.163 153.797 109.54 147.284 133.866C133.171 186.543 122.623 297.706 209.867 321.097L211.655 321.5L225.27 269.163ZM166.497 172.756C166.497 172.756 180.246 151.372 203.565 158C226.899 164.628 228.706 190.425 228.706 190.425L166.497 172.756Z" fill="#1D8D22"/>
<path d="M141.946 245.451L131.072 248.537C133.641 263.019 138.169 276.917 145.276 289.195C146.513 288.922 147.74 288.687 149 288.342C152.302 287.451 155.364 286.348 158.312 285.145C150.371 273.361 145.118 259.789 141.946 245.451ZM137.7 143.451C132.112 164.307 127.113 194.326 128.489 224.436C130.952 223.367 133.554 222.371 136.444 221.551L138.457 221.101C136.003 188.939 141.308 156.165 147.284 133.866C148.799 128.225 150.318 122.978 151.832 118.085C149.393 119.637 146.767 121.228 143.776 122.867C141.759 129.093 139.722 135.898 137.7 143.451Z" fill="#C04B41"/>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<script type="module" crossorigin src="./snapshot.v8KI4P3m.js"></script>
<link rel="modulepreload" crossorigin href="./assets/urlMatch-BYQrIQwR.js">
<body>
<iframe src="about:blank" sandbox="allow-same-origin allow-scripts" style="position:absolute;top:0;left:0;right:0;bottom:0;width:100%;height:100%;border:none;"></iframe>
</body>
</html>
@@ -0,0 +1,2 @@
import{i as c}from"./assets/urlMatch-BYQrIQwR.js";(async()=>{if(!navigator.serviceWorker)throw new Error(`Service workers are not supported.
Make sure to serve the Trace Viewer (${window.location}) via HTTPS or localhost.`);navigator.serviceWorker.register("sw.bundle.js"),navigator.serviceWorker.controller||await new Promise(t=>navigator.serviceWorker.oncontrollerchange=t);const e=new URL(location.href).searchParams.get("trace"),o=new URLSearchParams;e&&o.set("trace",e),await fetch("contexts?"+o.toString());const r=new URLSearchParams(location.search).get("r");if(!r||!c(r,location.href))return;const a=document.querySelector("iframe");a&&(a.src=r)})();
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en" translate="no">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="./playwright-logo.svg" type="image/svg+xml">
<title>Playwright Test</title>
<script type="module" crossorigin src="./uiMode.C2Efnu2P.js"></script>
<link rel="modulepreload" crossorigin href="./assets/urlMatch-BYQrIQwR.js">
<link rel="modulepreload" crossorigin href="./assets/defaultSettingsView-D31xz8zv.js">
<link rel="stylesheet" crossorigin href="./defaultSettingsView.BDKsFU3c.css">
<link rel="stylesheet" crossorigin href="./uiMode.Btcz36p_.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
@@ -0,0 +1,32 @@
/**
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
* https://github.com/chjj/term.js
* @license MIT
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* Originally forked from (with the author's permission):
* Fabrice Bellard's javascript vt100 for jslinux:
* http://bellard.org/jslinux/
* Copyright (c) 2011 Fabrice Bellard
* The original design remains. The terminal itself
* has been extended to include xterm CSI codes, among
* other features.
*/.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .xterm-accessibility-tree:not(.debug) *::selection{color:transparent}.xterm .xterm-accessibility-tree{-webkit-user-select:text;user-select:text;white-space:pre}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}
+79
View File
@@ -0,0 +1,79 @@
{
"name": "video-view-manager-e2e",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "video-view-manager-e2e",
"version": "1.0.0",
"dependencies": {
"@playwright/test": "^1.40.0",
"playwright": "^1.40.0"
},
"devDependencies": {},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@playwright/test": {
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz",
"integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==",
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.60.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright": {
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
"integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.60.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
"integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
}
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"status": "interrupted",
"failedTests": [
"df48abf4277a6018417c-8bd1501dbaa88575b602",
"df48abf4277a6018417c-f16674a8087f47ae76f3",
"df48abf4277a6018417c-594ea5fe297c882f7617",
"df48abf4277a6018417c-1f40d9c85b33526787e9",
"df48abf4277a6018417c-3cd07aeb462ec694e802"
]
}
@@ -0,0 +1,187 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: specs/epic-1-visibility.spec.js >> Epic 1: Core Camera Visibility Control >> FR-4: AV Tile Visual State Indicators >> Hidden state shows grey overlay + lock icon + tooltip
- Location: specs/epic-1-visibility.spec.js:115:5
# Error details
```
Test timeout of 120000ms exceeded while running "beforeEach" hook.
```
```
Error: page.waitForFunction: Test timeout of 120000ms exceeded.
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- list:
- listitem [ref=e2]:
- text:
- paragraph [ref=e3]: Foundry Virtual Tabletop nécessite une résolution d'écran de 1366px par 768px ou plus. Votre écran a une résolution de 1280px par 720px. Vous devez augmenter votre résolution ou utiliser un autre périphérique d'affichage, sinon certaines fonctionnalités du logiciel ne fonctionneront pas correctement.
- text:
- banner [ref=e5]:
- heading "Donjon & Cie" [level=1] [ref=e6]
- generic [ref=e7]:
- generic [ref=e8]:
- heading "Rejoindre la partie" [level=2] [ref=e9]
- combobox [ref=e14] [cursor=pointer]:
- option [selected]
- option "Gamemaster" [disabled]
- textbox "Mot de passe" [ref=e19]
- contentinfo [ref=e20]:
- button "Rejoindre la partie" [ref=e21] [cursor=pointer]:
- generic: Rejoindre la partie
- generic [ref=e22]:
- heading "Détails de la session" [level=2] [ref=e23]
- generic [ref=e25]: Prochaine partie
- generic [ref=e27]:
- generic [ref=e28]: Joueurs présents
- generic [ref=e30]:
- generic [ref=e31]: "1"
- generic [ref=e32]: /
- generic [ref=e33]: "1"
- generic [ref=e34]:
- heading "Retour à laccueil" [level=2] [ref=e35]
- textbox [ref=e39]
- contentinfo [ref=e40]:
- button "Retour à laccueil" [ref=e41] [cursor=pointer]:
- generic: Retour à laccueil
- article [ref=e42]:
- heading "Description du monde" [level=2] [ref=e43]
- contentinfo [ref=e44]:
- paragraph [ref=e45]: Version 14 Build 361
```
# Test source
```ts
1 | /**
2 | * Helpers pour les tests E2E avec FoundryVTT
3 | *
4 | * Fournit des fonctions utilitaires pour :
5 | * - Attendre que Foundry soit prêt
6 | * - Attendre que le module soit chargé
7 | * - Interagir avec l'UI FoundryVTT
8 | * - Gérer les sélecteurs spécifiques au module
9 | */
10 |
11 | import { expect } from '@playwright/test';
12 |
13 | /**
14 | * Attend que FoundryVTT soit complètement chargé
15 | * @param {import('@playwright/test').Page} page - La page Playwright
16 | * @param {number} timeout - Timeout en ms (défaut: 30000)
17 | */
18 | export async function waitForFoundryReady(page, timeout = 30000) {
> 19 | await page.waitForFunction(() => {
| ^ Error: page.waitForFunction: Test timeout of 120000ms exceeded.
20 | return typeof game !== 'undefined' && game.ready;
21 | }, { timeout });
22 | }
23 |
24 | /**
25 | * Attend que le module Video View Manager soit actif
26 | * @param {import('@playwright/test').Page} page - La page Playwright
27 | * @param {number} timeout - Timeout en ms (défaut: 15000)
28 | */
29 | export async function waitForVVMModule(page, timeout = 15000) {
30 | await page.waitForFunction(() => {
31 | const module = game.modules?.get?.('video-view-manager');
32 | return module?.active === true;
33 | }, { timeout });
34 | }
35 |
36 | /**
37 | * Attend qu'un élément du module soit présent
38 | * @param {import('@playwright/test').Page} page - La page Playwright
39 | * @param {string} selector - Sélecteur CSS
40 | * @param {number} timeout - Timeout en ms (défaut: 10000)
41 | */
42 | export async function waitForVVMElement(page, selector, timeout = 10000) {
43 | await page.waitForSelector(selector, {
44 | state: 'visible',
45 | timeout
46 | });
47 | }
48 |
49 | /**
50 | * Clique sur un bouton dans l'UI Foundry avec retry
51 | * @param {import('@playwright/test').Page} page - La page Playwright
52 | * @param {string|import('@playwright/test').Locator} button - Sélecteur ou Locator
53 | * @param {number} retries - Nombre de tentatives (défaut: 3)
54 | */
55 | export async function clickFoundryButton(page, button, retries = 3) {
56 | for (let i = 0; i < retries; i++) {
57 | try {
58 | const locator = typeof button === 'string' ? page.locator(button) : button;
59 | await locator.click({ timeout: 5000 });
60 | return;
61 | } catch (error) {
62 | if (i === retries - 1) throw error;
63 | await page.waitForTimeout(1000);
64 | }
65 | }
66 | }
67 |
68 | /**
69 | * Ouvre le sidebar de configuration Foundry
70 | * @param {import('@playwright/test').Page} page - La page Playwright
71 | */
72 | export async function openFoundrySidebar(page) {
73 | await clickFoundryButton(page, 'button[aria-label="Configure Settings"]');
74 | await page.waitForSelector('.app-v2.settings', { state: 'visible', timeout: 10000 });
75 | }
76 |
77 | /**
78 | * Ouvre le Director's Board (Epic 2)
79 | * @param {import('@playwright/test').Page} page - La page Playwright
80 | */
81 | export async function openDirectorsBoard(page) {
82 | // Le Director's Board a un bouton dédié dans la sidebar
83 | await page.waitForSelector('button[aria-label*="Director\'s Board"]', { timeout: 10000 });
84 | await clickFoundryButton(page, 'button[aria-label*="Director\'s Board"]');
85 |
86 | // Attendre que le board soit ouvert
87 | await page.waitForSelector('.scrying-pool-directors-board', {
88 | state: 'visible',
89 | timeout: 10000
90 | });
91 | }
92 |
93 | /**
94 | * Ouvre le Player Privacy Panel pour un utilisateur
95 | * @param {import('@playwright/test').Page} page - La page Playwright
96 | * @param {string} userId - L'ID de l'utilisateur
97 | */
98 | export async function openPlayerPrivacyPanel(page, userId) {
99 | // Le panel s'ouvre via les paramètres du module
100 | await openFoundrySidebar(page);
101 |
102 | // Naviguer vers les paramètres du module
103 | await clickFoundryButton(page, 'button:has-text("Video View Manager")');
104 | await page.waitForTimeout(1000);
105 |
106 | // Cliquer sur le bouton Player Privacy
107 | await clickFoundryButton(page, 'button:has-text("Player Privacy")');
108 |
109 | // Attendre le panel
110 | await page.waitForSelector('.sp-player-privacy-panel', {
111 | state: 'visible',
112 | timeout: 10000
113 | });
114 | }
115 |
116 | /**
117 | * Sélectionne un utilisateur dans une liste Foundry
118 | * @param {import('@playwright/test').Page} page - La page Playwright
119 | * @param {string} username - Le nom de l'utilisateur
```
@@ -0,0 +1,187 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: specs/epic-1-visibility.spec.js >> Epic 1: Core Camera Visibility Control >> FR-4: AV Tile Visual State Indicators >> Hidden state shows grey overlay + lock icon + tooltip
- Location: specs/epic-1-visibility.spec.js:115:5
# Error details
```
Test timeout of 120000ms exceeded while running "beforeEach" hook.
```
```
Error: page.waitForFunction: Test timeout of 120000ms exceeded.
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- list:
- listitem [ref=e2]:
- text:
- paragraph [ref=e3]: Foundry Virtual Tabletop nécessite une résolution d'écran de 1366px par 768px ou plus. Votre écran a une résolution de 1280px par 720px. Vous devez augmenter votre résolution ou utiliser un autre périphérique d'affichage, sinon certaines fonctionnalités du logiciel ne fonctionneront pas correctement.
- text:
- banner [ref=e5]:
- heading "Donjon & Cie" [level=1] [ref=e6]
- generic [ref=e7]:
- generic [ref=e8]:
- heading "Rejoindre la partie" [level=2] [ref=e9]
- combobox [ref=e14] [cursor=pointer]:
- option [selected]
- option "Gamemaster" [disabled]
- textbox "Mot de passe" [ref=e19]
- contentinfo [ref=e20]:
- button "Rejoindre la partie" [ref=e21] [cursor=pointer]:
- generic: Rejoindre la partie
- generic [ref=e22]:
- heading "Détails de la session" [level=2] [ref=e23]
- generic [ref=e25]: Prochaine partie
- generic [ref=e27]:
- generic [ref=e28]: Joueurs présents
- generic [ref=e30]:
- generic [ref=e31]: "1"
- generic [ref=e32]: /
- generic [ref=e33]: "1"
- generic [ref=e34]:
- heading "Retour à laccueil" [level=2] [ref=e35]
- textbox [ref=e39]
- contentinfo [ref=e40]:
- button "Retour à laccueil" [ref=e41] [cursor=pointer]:
- generic: Retour à laccueil
- article [ref=e42]:
- heading "Description du monde" [level=2] [ref=e43]
- contentinfo [ref=e44]:
- paragraph [ref=e45]: Version 14 Build 361
```
# Test source
```ts
1 | /**
2 | * Helpers pour les tests E2E avec FoundryVTT
3 | *
4 | * Fournit des fonctions utilitaires pour :
5 | * - Attendre que Foundry soit prêt
6 | * - Attendre que le module soit chargé
7 | * - Interagir avec l'UI FoundryVTT
8 | * - Gérer les sélecteurs spécifiques au module
9 | */
10 |
11 | import { expect } from '@playwright/test';
12 |
13 | /**
14 | * Attend que FoundryVTT soit complètement chargé
15 | * @param {import('@playwright/test').Page} page - La page Playwright
16 | * @param {number} timeout - Timeout en ms (défaut: 30000)
17 | */
18 | export async function waitForFoundryReady(page, timeout = 30000) {
> 19 | await page.waitForFunction(() => {
| ^ Error: page.waitForFunction: Test timeout of 120000ms exceeded.
20 | return typeof game !== 'undefined' && game.ready;
21 | }, { timeout });
22 | }
23 |
24 | /**
25 | * Attend que le module Video View Manager soit actif
26 | * @param {import('@playwright/test').Page} page - La page Playwright
27 | * @param {number} timeout - Timeout en ms (défaut: 15000)
28 | */
29 | export async function waitForVVMModule(page, timeout = 15000) {
30 | await page.waitForFunction(() => {
31 | const module = game.modules?.get?.('video-view-manager');
32 | return module?.active === true;
33 | }, { timeout });
34 | }
35 |
36 | /**
37 | * Attend qu'un élément du module soit présent
38 | * @param {import('@playwright/test').Page} page - La page Playwright
39 | * @param {string} selector - Sélecteur CSS
40 | * @param {number} timeout - Timeout en ms (défaut: 10000)
41 | */
42 | export async function waitForVVMElement(page, selector, timeout = 10000) {
43 | await page.waitForSelector(selector, {
44 | state: 'visible',
45 | timeout
46 | });
47 | }
48 |
49 | /**
50 | * Clique sur un bouton dans l'UI Foundry avec retry
51 | * @param {import('@playwright/test').Page} page - La page Playwright
52 | * @param {string|import('@playwright/test').Locator} button - Sélecteur ou Locator
53 | * @param {number} retries - Nombre de tentatives (défaut: 3)
54 | */
55 | export async function clickFoundryButton(page, button, retries = 3) {
56 | for (let i = 0; i < retries; i++) {
57 | try {
58 | const locator = typeof button === 'string' ? page.locator(button) : button;
59 | await locator.click({ timeout: 5000 });
60 | return;
61 | } catch (error) {
62 | if (i === retries - 1) throw error;
63 | await page.waitForTimeout(1000);
64 | }
65 | }
66 | }
67 |
68 | /**
69 | * Ouvre le sidebar de configuration Foundry
70 | * @param {import('@playwright/test').Page} page - La page Playwright
71 | */
72 | export async function openFoundrySidebar(page) {
73 | await clickFoundryButton(page, 'button[aria-label="Configure Settings"]');
74 | await page.waitForSelector('.app-v2.settings', { state: 'visible', timeout: 10000 });
75 | }
76 |
77 | /**
78 | * Ouvre le Director's Board (Epic 2)
79 | * @param {import('@playwright/test').Page} page - La page Playwright
80 | */
81 | export async function openDirectorsBoard(page) {
82 | // Le Director's Board a un bouton dédié dans la sidebar
83 | await page.waitForSelector('button[aria-label*="Director\'s Board"]', { timeout: 10000 });
84 | await clickFoundryButton(page, 'button[aria-label*="Director\'s Board"]');
85 |
86 | // Attendre que le board soit ouvert
87 | await page.waitForSelector('.scrying-pool-directors-board', {
88 | state: 'visible',
89 | timeout: 10000
90 | });
91 | }
92 |
93 | /**
94 | * Ouvre le Player Privacy Panel pour un utilisateur
95 | * @param {import('@playwright/test').Page} page - La page Playwright
96 | * @param {string} userId - L'ID de l'utilisateur
97 | */
98 | export async function openPlayerPrivacyPanel(page, userId) {
99 | // Le panel s'ouvre via les paramètres du module
100 | await openFoundrySidebar(page);
101 |
102 | // Naviguer vers les paramètres du module
103 | await clickFoundryButton(page, 'button:has-text("Video View Manager")');
104 | await page.waitForTimeout(1000);
105 |
106 | // Cliquer sur le bouton Player Privacy
107 | await clickFoundryButton(page, 'button:has-text("Player Privacy")');
108 |
109 | // Attendre le panel
110 | await page.waitForSelector('.sp-player-privacy-panel', {
111 | state: 'visible',
112 | timeout: 10000
113 | });
114 | }
115 |
116 | /**
117 | * Sélectionne un utilisateur dans une liste Foundry
118 | * @param {import('@playwright/test').Page} page - La page Playwright
119 | * @param {string} username - Le nom de l'utilisateur
```
@@ -0,0 +1,187 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: specs/epic-1-visibility.spec.js >> Epic 1: Core Camera Visibility Control >> FR-4: AV Tile Visual State Indicators >> Hidden state shows grey overlay + lock icon + tooltip
- Location: specs/epic-1-visibility.spec.js:115:5
# Error details
```
Test timeout of 120000ms exceeded while running "beforeEach" hook.
```
```
Error: page.waitForFunction: Test timeout of 120000ms exceeded.
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- list:
- listitem [ref=e2]:
- text:
- paragraph [ref=e3]: Foundry Virtual Tabletop nécessite une résolution d'écran de 1366px par 768px ou plus. Votre écran a une résolution de 1280px par 720px. Vous devez augmenter votre résolution ou utiliser un autre périphérique d'affichage, sinon certaines fonctionnalités du logiciel ne fonctionneront pas correctement.
- text:
- banner [ref=e5]:
- heading "Donjon & Cie" [level=1] [ref=e6]
- generic [ref=e7]:
- generic [ref=e8]:
- heading "Rejoindre la partie" [level=2] [ref=e9]
- combobox [ref=e14] [cursor=pointer]:
- option [selected]
- option "Gamemaster" [disabled]
- textbox "Mot de passe" [ref=e19]
- contentinfo [ref=e20]:
- button "Rejoindre la partie" [ref=e21] [cursor=pointer]:
- generic: Rejoindre la partie
- generic [ref=e22]:
- heading "Détails de la session" [level=2] [ref=e23]
- generic [ref=e25]: Prochaine partie
- generic [ref=e27]:
- generic [ref=e28]: Joueurs présents
- generic [ref=e30]:
- generic [ref=e31]: "1"
- generic [ref=e32]: /
- generic [ref=e33]: "1"
- generic [ref=e34]:
- heading "Retour à laccueil" [level=2] [ref=e35]
- textbox [ref=e39]
- contentinfo [ref=e40]:
- button "Retour à laccueil" [ref=e41] [cursor=pointer]:
- generic: Retour à laccueil
- article [ref=e42]:
- heading "Description du monde" [level=2] [ref=e43]
- contentinfo [ref=e44]:
- paragraph [ref=e45]: Version 14 Build 361
```
# Test source
```ts
1 | /**
2 | * Helpers pour les tests E2E avec FoundryVTT
3 | *
4 | * Fournit des fonctions utilitaires pour :
5 | * - Attendre que Foundry soit prêt
6 | * - Attendre que le module soit chargé
7 | * - Interagir avec l'UI FoundryVTT
8 | * - Gérer les sélecteurs spécifiques au module
9 | */
10 |
11 | import { expect } from '@playwright/test';
12 |
13 | /**
14 | * Attend que FoundryVTT soit complètement chargé
15 | * @param {import('@playwright/test').Page} page - La page Playwright
16 | * @param {number} timeout - Timeout en ms (défaut: 30000)
17 | */
18 | export async function waitForFoundryReady(page, timeout = 30000) {
> 19 | await page.waitForFunction(() => {
| ^ Error: page.waitForFunction: Test timeout of 120000ms exceeded.
20 | return typeof game !== 'undefined' && game.ready;
21 | }, { timeout });
22 | }
23 |
24 | /**
25 | * Attend que le module Video View Manager soit actif
26 | * @param {import('@playwright/test').Page} page - La page Playwright
27 | * @param {number} timeout - Timeout en ms (défaut: 15000)
28 | */
29 | export async function waitForVVMModule(page, timeout = 15000) {
30 | await page.waitForFunction(() => {
31 | const module = game.modules?.get?.('video-view-manager');
32 | return module?.active === true;
33 | }, { timeout });
34 | }
35 |
36 | /**
37 | * Attend qu'un élément du module soit présent
38 | * @param {import('@playwright/test').Page} page - La page Playwright
39 | * @param {string} selector - Sélecteur CSS
40 | * @param {number} timeout - Timeout en ms (défaut: 10000)
41 | */
42 | export async function waitForVVMElement(page, selector, timeout = 10000) {
43 | await page.waitForSelector(selector, {
44 | state: 'visible',
45 | timeout
46 | });
47 | }
48 |
49 | /**
50 | * Clique sur un bouton dans l'UI Foundry avec retry
51 | * @param {import('@playwright/test').Page} page - La page Playwright
52 | * @param {string|import('@playwright/test').Locator} button - Sélecteur ou Locator
53 | * @param {number} retries - Nombre de tentatives (défaut: 3)
54 | */
55 | export async function clickFoundryButton(page, button, retries = 3) {
56 | for (let i = 0; i < retries; i++) {
57 | try {
58 | const locator = typeof button === 'string' ? page.locator(button) : button;
59 | await locator.click({ timeout: 5000 });
60 | return;
61 | } catch (error) {
62 | if (i === retries - 1) throw error;
63 | await page.waitForTimeout(1000);
64 | }
65 | }
66 | }
67 |
68 | /**
69 | * Ouvre le sidebar de configuration Foundry
70 | * @param {import('@playwright/test').Page} page - La page Playwright
71 | */
72 | export async function openFoundrySidebar(page) {
73 | await clickFoundryButton(page, 'button[aria-label="Configure Settings"]');
74 | await page.waitForSelector('.app-v2.settings', { state: 'visible', timeout: 10000 });
75 | }
76 |
77 | /**
78 | * Ouvre le Director's Board (Epic 2)
79 | * @param {import('@playwright/test').Page} page - La page Playwright
80 | */
81 | export async function openDirectorsBoard(page) {
82 | // Le Director's Board a un bouton dédié dans la sidebar
83 | await page.waitForSelector('button[aria-label*="Director\'s Board"]', { timeout: 10000 });
84 | await clickFoundryButton(page, 'button[aria-label*="Director\'s Board"]');
85 |
86 | // Attendre que le board soit ouvert
87 | await page.waitForSelector('.scrying-pool-directors-board', {
88 | state: 'visible',
89 | timeout: 10000
90 | });
91 | }
92 |
93 | /**
94 | * Ouvre le Player Privacy Panel pour un utilisateur
95 | * @param {import('@playwright/test').Page} page - La page Playwright
96 | * @param {string} userId - L'ID de l'utilisateur
97 | */
98 | export async function openPlayerPrivacyPanel(page, userId) {
99 | // Le panel s'ouvre via les paramètres du module
100 | await openFoundrySidebar(page);
101 |
102 | // Naviguer vers les paramètres du module
103 | await clickFoundryButton(page, 'button:has-text("Video View Manager")');
104 | await page.waitForTimeout(1000);
105 |
106 | // Cliquer sur le bouton Player Privacy
107 | await clickFoundryButton(page, 'button:has-text("Player Privacy")');
108 |
109 | // Attendre le panel
110 | await page.waitForSelector('.sp-player-privacy-panel', {
111 | state: 'visible',
112 | timeout: 10000
113 | });
114 | }
115 |
116 | /**
117 | * Sélectionne un utilisateur dans une liste Foundry
118 | * @param {import('@playwright/test').Page} page - La page Playwright
119 | * @param {string} username - Le nom de l'utilisateur
```
@@ -0,0 +1,187 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: specs/epic-1-visibility.spec.js >> Epic 1: Core Camera Visibility Control >> FR-4: AV Tile Visual State Indicators >> Self-muted state shows camera-off icon
- Location: specs/epic-1-visibility.spec.js:132:5
# Error details
```
Test timeout of 120000ms exceeded while running "beforeEach" hook.
```
```
Error: page.waitForFunction: Test timeout of 120000ms exceeded.
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- list:
- listitem [ref=e2]:
- text:
- paragraph [ref=e3]: Foundry Virtual Tabletop nécessite une résolution d'écran de 1366px par 768px ou plus. Votre écran a une résolution de 1280px par 720px. Vous devez augmenter votre résolution ou utiliser un autre périphérique d'affichage, sinon certaines fonctionnalités du logiciel ne fonctionneront pas correctement.
- text:
- banner [ref=e5]:
- heading "Donjon & Cie" [level=1] [ref=e6]
- generic [ref=e7]:
- generic [ref=e8]:
- heading "Rejoindre la partie" [level=2] [ref=e9]
- combobox [ref=e14] [cursor=pointer]:
- option [selected]
- option "Gamemaster" [disabled]
- textbox "Mot de passe" [ref=e19]
- contentinfo [ref=e20]:
- button "Rejoindre la partie" [ref=e21] [cursor=pointer]:
- generic: Rejoindre la partie
- generic [ref=e22]:
- heading "Détails de la session" [level=2] [ref=e23]
- generic [ref=e25]: Prochaine partie
- generic [ref=e27]:
- generic [ref=e28]: Joueurs présents
- generic [ref=e30]:
- generic [ref=e31]: "1"
- generic [ref=e32]: /
- generic [ref=e33]: "1"
- generic [ref=e34]:
- heading "Retour à laccueil" [level=2] [ref=e35]
- textbox [ref=e39]
- contentinfo [ref=e40]:
- button "Retour à laccueil" [ref=e41] [cursor=pointer]:
- generic: Retour à laccueil
- article [ref=e42]:
- heading "Description du monde" [level=2] [ref=e43]
- contentinfo [ref=e44]:
- paragraph [ref=e45]: Version 14 Build 361
```
# Test source
```ts
1 | /**
2 | * Helpers pour les tests E2E avec FoundryVTT
3 | *
4 | * Fournit des fonctions utilitaires pour :
5 | * - Attendre que Foundry soit prêt
6 | * - Attendre que le module soit chargé
7 | * - Interagir avec l'UI FoundryVTT
8 | * - Gérer les sélecteurs spécifiques au module
9 | */
10 |
11 | import { expect } from '@playwright/test';
12 |
13 | /**
14 | * Attend que FoundryVTT soit complètement chargé
15 | * @param {import('@playwright/test').Page} page - La page Playwright
16 | * @param {number} timeout - Timeout en ms (défaut: 30000)
17 | */
18 | export async function waitForFoundryReady(page, timeout = 30000) {
> 19 | await page.waitForFunction(() => {
| ^ Error: page.waitForFunction: Test timeout of 120000ms exceeded.
20 | return typeof game !== 'undefined' && game.ready;
21 | }, { timeout });
22 | }
23 |
24 | /**
25 | * Attend que le module Video View Manager soit actif
26 | * @param {import('@playwright/test').Page} page - La page Playwright
27 | * @param {number} timeout - Timeout en ms (défaut: 15000)
28 | */
29 | export async function waitForVVMModule(page, timeout = 15000) {
30 | await page.waitForFunction(() => {
31 | const module = game.modules?.get?.('video-view-manager');
32 | return module?.active === true;
33 | }, { timeout });
34 | }
35 |
36 | /**
37 | * Attend qu'un élément du module soit présent
38 | * @param {import('@playwright/test').Page} page - La page Playwright
39 | * @param {string} selector - Sélecteur CSS
40 | * @param {number} timeout - Timeout en ms (défaut: 10000)
41 | */
42 | export async function waitForVVMElement(page, selector, timeout = 10000) {
43 | await page.waitForSelector(selector, {
44 | state: 'visible',
45 | timeout
46 | });
47 | }
48 |
49 | /**
50 | * Clique sur un bouton dans l'UI Foundry avec retry
51 | * @param {import('@playwright/test').Page} page - La page Playwright
52 | * @param {string|import('@playwright/test').Locator} button - Sélecteur ou Locator
53 | * @param {number} retries - Nombre de tentatives (défaut: 3)
54 | */
55 | export async function clickFoundryButton(page, button, retries = 3) {
56 | for (let i = 0; i < retries; i++) {
57 | try {
58 | const locator = typeof button === 'string' ? page.locator(button) : button;
59 | await locator.click({ timeout: 5000 });
60 | return;
61 | } catch (error) {
62 | if (i === retries - 1) throw error;
63 | await page.waitForTimeout(1000);
64 | }
65 | }
66 | }
67 |
68 | /**
69 | * Ouvre le sidebar de configuration Foundry
70 | * @param {import('@playwright/test').Page} page - La page Playwright
71 | */
72 | export async function openFoundrySidebar(page) {
73 | await clickFoundryButton(page, 'button[aria-label="Configure Settings"]');
74 | await page.waitForSelector('.app-v2.settings', { state: 'visible', timeout: 10000 });
75 | }
76 |
77 | /**
78 | * Ouvre le Director's Board (Epic 2)
79 | * @param {import('@playwright/test').Page} page - La page Playwright
80 | */
81 | export async function openDirectorsBoard(page) {
82 | // Le Director's Board a un bouton dédié dans la sidebar
83 | await page.waitForSelector('button[aria-label*="Director\'s Board"]', { timeout: 10000 });
84 | await clickFoundryButton(page, 'button[aria-label*="Director\'s Board"]');
85 |
86 | // Attendre que le board soit ouvert
87 | await page.waitForSelector('.scrying-pool-directors-board', {
88 | state: 'visible',
89 | timeout: 10000
90 | });
91 | }
92 |
93 | /**
94 | * Ouvre le Player Privacy Panel pour un utilisateur
95 | * @param {import('@playwright/test').Page} page - La page Playwright
96 | * @param {string} userId - L'ID de l'utilisateur
97 | */
98 | export async function openPlayerPrivacyPanel(page, userId) {
99 | // Le panel s'ouvre via les paramètres du module
100 | await openFoundrySidebar(page);
101 |
102 | // Naviguer vers les paramètres du module
103 | await clickFoundryButton(page, 'button:has-text("Video View Manager")');
104 | await page.waitForTimeout(1000);
105 |
106 | // Cliquer sur le bouton Player Privacy
107 | await clickFoundryButton(page, 'button:has-text("Player Privacy")');
108 |
109 | // Attendre le panel
110 | await page.waitForSelector('.sp-player-privacy-panel', {
111 | state: 'visible',
112 | timeout: 10000
113 | });
114 | }
115 |
116 | /**
117 | * Sélectionne un utilisateur dans une liste Foundry
118 | * @param {import('@playwright/test').Page} page - La page Playwright
119 | * @param {string} username - Le nom de l'utilisateur
```
@@ -0,0 +1,141 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: specs/epic-1-visibility.spec.js >> Epic 1: Core Camera Visibility Control >> FR-4: AV Tile Visual State Indicators >> Self-muted state shows camera-off icon
- Location: specs/epic-1-visibility.spec.js:132:5
# Error details
```
Error: page.waitForFunction: Test ended.
```
# Test source
```ts
1 | /**
2 | * Helpers pour les tests E2E avec FoundryVTT
3 | *
4 | * Fournit des fonctions utilitaires pour :
5 | * - Attendre que Foundry soit prêt
6 | * - Attendre que le module soit chargé
7 | * - Interagir avec l'UI FoundryVTT
8 | * - Gérer les sélecteurs spécifiques au module
9 | */
10 |
11 | import { expect } from '@playwright/test';
12 |
13 | /**
14 | * Attend que FoundryVTT soit complètement chargé
15 | * @param {import('@playwright/test').Page} page - La page Playwright
16 | * @param {number} timeout - Timeout en ms (défaut: 30000)
17 | */
18 | export async function waitForFoundryReady(page, timeout = 30000) {
> 19 | await page.waitForFunction(() => {
| ^ Error: page.waitForFunction: Test ended.
20 | return typeof game !== 'undefined' && game.ready;
21 | }, { timeout });
22 | }
23 |
24 | /**
25 | * Attend que le module Video View Manager soit actif
26 | * @param {import('@playwright/test').Page} page - La page Playwright
27 | * @param {number} timeout - Timeout en ms (défaut: 15000)
28 | */
29 | export async function waitForVVMModule(page, timeout = 15000) {
30 | await page.waitForFunction(() => {
31 | const module = game.modules?.get?.('video-view-manager');
32 | return module?.active === true;
33 | }, { timeout });
34 | }
35 |
36 | /**
37 | * Attend qu'un élément du module soit présent
38 | * @param {import('@playwright/test').Page} page - La page Playwright
39 | * @param {string} selector - Sélecteur CSS
40 | * @param {number} timeout - Timeout en ms (défaut: 10000)
41 | */
42 | export async function waitForVVMElement(page, selector, timeout = 10000) {
43 | await page.waitForSelector(selector, {
44 | state: 'visible',
45 | timeout
46 | });
47 | }
48 |
49 | /**
50 | * Clique sur un bouton dans l'UI Foundry avec retry
51 | * @param {import('@playwright/test').Page} page - La page Playwright
52 | * @param {string|import('@playwright/test').Locator} button - Sélecteur ou Locator
53 | * @param {number} retries - Nombre de tentatives (défaut: 3)
54 | */
55 | export async function clickFoundryButton(page, button, retries = 3) {
56 | for (let i = 0; i < retries; i++) {
57 | try {
58 | const locator = typeof button === 'string' ? page.locator(button) : button;
59 | await locator.click({ timeout: 5000 });
60 | return;
61 | } catch (error) {
62 | if (i === retries - 1) throw error;
63 | await page.waitForTimeout(1000);
64 | }
65 | }
66 | }
67 |
68 | /**
69 | * Ouvre le sidebar de configuration Foundry
70 | * @param {import('@playwright/test').Page} page - La page Playwright
71 | */
72 | export async function openFoundrySidebar(page) {
73 | await clickFoundryButton(page, 'button[aria-label="Configure Settings"]');
74 | await page.waitForSelector('.app-v2.settings', { state: 'visible', timeout: 10000 });
75 | }
76 |
77 | /**
78 | * Ouvre le Director's Board (Epic 2)
79 | * @param {import('@playwright/test').Page} page - La page Playwright
80 | */
81 | export async function openDirectorsBoard(page) {
82 | // Le Director's Board a un bouton dédié dans la sidebar
83 | await page.waitForSelector('button[aria-label*="Director\'s Board"]', { timeout: 10000 });
84 | await clickFoundryButton(page, 'button[aria-label*="Director\'s Board"]');
85 |
86 | // Attendre que le board soit ouvert
87 | await page.waitForSelector('.scrying-pool-directors-board', {
88 | state: 'visible',
89 | timeout: 10000
90 | });
91 | }
92 |
93 | /**
94 | * Ouvre le Player Privacy Panel pour un utilisateur
95 | * @param {import('@playwright/test').Page} page - La page Playwright
96 | * @param {string} userId - L'ID de l'utilisateur
97 | */
98 | export async function openPlayerPrivacyPanel(page, userId) {
99 | // Le panel s'ouvre via les paramètres du module
100 | await openFoundrySidebar(page);
101 |
102 | // Naviguer vers les paramètres du module
103 | await clickFoundryButton(page, 'button:has-text("Video View Manager")');
104 | await page.waitForTimeout(1000);
105 |
106 | // Cliquer sur le bouton Player Privacy
107 | await clickFoundryButton(page, 'button:has-text("Player Privacy")');
108 |
109 | // Attendre le panel
110 | await page.waitForSelector('.sp-player-privacy-panel', {
111 | state: 'visible',
112 | timeout: 10000
113 | });
114 | }
115 |
116 | /**
117 | * Sélectionne un utilisateur dans une liste Foundry
118 | * @param {import('@playwright/test').Page} page - La page Playwright
119 | * @param {string} username - Le nom de l'utilisateur
```
@@ -0,0 +1,187 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: specs/epic-1-visibility.spec.js >> Epic 1: Core Camera Visibility Control >> FR-4: AV Tile Visual State Indicators >> Self-muted state shows camera-off icon
- Location: specs/epic-1-visibility.spec.js:132:5
# Error details
```
Test timeout of 120000ms exceeded while running "beforeEach" hook.
```
```
Error: page.waitForFunction: Test timeout of 120000ms exceeded.
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- list:
- listitem [ref=e2]:
- text:
- paragraph [ref=e3]: Foundry Virtual Tabletop nécessite une résolution d'écran de 1366px par 768px ou plus. Votre écran a une résolution de 1280px par 720px. Vous devez augmenter votre résolution ou utiliser un autre périphérique d'affichage, sinon certaines fonctionnalités du logiciel ne fonctionneront pas correctement.
- text:
- banner [ref=e5]:
- heading "Donjon & Cie" [level=1] [ref=e6]
- generic [ref=e7]:
- generic [ref=e8]:
- heading "Rejoindre la partie" [level=2] [ref=e9]
- combobox [ref=e14] [cursor=pointer]:
- option [selected]
- option "Gamemaster" [disabled]
- textbox "Mot de passe" [ref=e19]
- contentinfo [ref=e20]:
- button "Rejoindre la partie" [ref=e21] [cursor=pointer]:
- generic: Rejoindre la partie
- generic [ref=e22]:
- heading "Détails de la session" [level=2] [ref=e23]
- generic [ref=e25]: Prochaine partie
- generic [ref=e27]:
- generic [ref=e28]: Joueurs présents
- generic [ref=e30]:
- generic [ref=e31]: "1"
- generic [ref=e32]: /
- generic [ref=e33]: "1"
- generic [ref=e34]:
- heading "Retour à laccueil" [level=2] [ref=e35]
- textbox [ref=e39]
- contentinfo [ref=e40]:
- button "Retour à laccueil" [ref=e41] [cursor=pointer]:
- generic: Retour à laccueil
- article [ref=e42]:
- heading "Description du monde" [level=2] [ref=e43]
- contentinfo [ref=e44]:
- paragraph [ref=e45]: Version 14 Build 361
```
# Test source
```ts
1 | /**
2 | * Helpers pour les tests E2E avec FoundryVTT
3 | *
4 | * Fournit des fonctions utilitaires pour :
5 | * - Attendre que Foundry soit prêt
6 | * - Attendre que le module soit chargé
7 | * - Interagir avec l'UI FoundryVTT
8 | * - Gérer les sélecteurs spécifiques au module
9 | */
10 |
11 | import { expect } from '@playwright/test';
12 |
13 | /**
14 | * Attend que FoundryVTT soit complètement chargé
15 | * @param {import('@playwright/test').Page} page - La page Playwright
16 | * @param {number} timeout - Timeout en ms (défaut: 30000)
17 | */
18 | export async function waitForFoundryReady(page, timeout = 30000) {
> 19 | await page.waitForFunction(() => {
| ^ Error: page.waitForFunction: Test timeout of 120000ms exceeded.
20 | return typeof game !== 'undefined' && game.ready;
21 | }, { timeout });
22 | }
23 |
24 | /**
25 | * Attend que le module Video View Manager soit actif
26 | * @param {import('@playwright/test').Page} page - La page Playwright
27 | * @param {number} timeout - Timeout en ms (défaut: 15000)
28 | */
29 | export async function waitForVVMModule(page, timeout = 15000) {
30 | await page.waitForFunction(() => {
31 | const module = game.modules?.get?.('video-view-manager');
32 | return module?.active === true;
33 | }, { timeout });
34 | }
35 |
36 | /**
37 | * Attend qu'un élément du module soit présent
38 | * @param {import('@playwright/test').Page} page - La page Playwright
39 | * @param {string} selector - Sélecteur CSS
40 | * @param {number} timeout - Timeout en ms (défaut: 10000)
41 | */
42 | export async function waitForVVMElement(page, selector, timeout = 10000) {
43 | await page.waitForSelector(selector, {
44 | state: 'visible',
45 | timeout
46 | });
47 | }
48 |
49 | /**
50 | * Clique sur un bouton dans l'UI Foundry avec retry
51 | * @param {import('@playwright/test').Page} page - La page Playwright
52 | * @param {string|import('@playwright/test').Locator} button - Sélecteur ou Locator
53 | * @param {number} retries - Nombre de tentatives (défaut: 3)
54 | */
55 | export async function clickFoundryButton(page, button, retries = 3) {
56 | for (let i = 0; i < retries; i++) {
57 | try {
58 | const locator = typeof button === 'string' ? page.locator(button) : button;
59 | await locator.click({ timeout: 5000 });
60 | return;
61 | } catch (error) {
62 | if (i === retries - 1) throw error;
63 | await page.waitForTimeout(1000);
64 | }
65 | }
66 | }
67 |
68 | /**
69 | * Ouvre le sidebar de configuration Foundry
70 | * @param {import('@playwright/test').Page} page - La page Playwright
71 | */
72 | export async function openFoundrySidebar(page) {
73 | await clickFoundryButton(page, 'button[aria-label="Configure Settings"]');
74 | await page.waitForSelector('.app-v2.settings', { state: 'visible', timeout: 10000 });
75 | }
76 |
77 | /**
78 | * Ouvre le Director's Board (Epic 2)
79 | * @param {import('@playwright/test').Page} page - La page Playwright
80 | */
81 | export async function openDirectorsBoard(page) {
82 | // Le Director's Board a un bouton dédié dans la sidebar
83 | await page.waitForSelector('button[aria-label*="Director\'s Board"]', { timeout: 10000 });
84 | await clickFoundryButton(page, 'button[aria-label*="Director\'s Board"]');
85 |
86 | // Attendre que le board soit ouvert
87 | await page.waitForSelector('.scrying-pool-directors-board', {
88 | state: 'visible',
89 | timeout: 10000
90 | });
91 | }
92 |
93 | /**
94 | * Ouvre le Player Privacy Panel pour un utilisateur
95 | * @param {import('@playwright/test').Page} page - La page Playwright
96 | * @param {string} userId - L'ID de l'utilisateur
97 | */
98 | export async function openPlayerPrivacyPanel(page, userId) {
99 | // Le panel s'ouvre via les paramètres du module
100 | await openFoundrySidebar(page);
101 |
102 | // Naviguer vers les paramètres du module
103 | await clickFoundryButton(page, 'button:has-text("Video View Manager")');
104 | await page.waitForTimeout(1000);
105 |
106 | // Cliquer sur le bouton Player Privacy
107 | await clickFoundryButton(page, 'button:has-text("Player Privacy")');
108 |
109 | // Attendre le panel
110 | await page.waitForSelector('.sp-player-privacy-panel', {
111 | state: 'visible',
112 | timeout: 10000
113 | });
114 | }
115 |
116 | /**
117 | * Sélectionne un utilisateur dans une liste Foundry
118 | * @param {import('@playwright/test').Page} page - La page Playwright
119 | * @param {string} username - Le nom de l'utilisateur
```
@@ -0,0 +1,187 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: specs/epic-1-visibility.spec.js >> Epic 1: Core Camera Visibility Control >> FR-1: Hide/Show Participant via Context Menu >> GM can show a hidden participant via right-click
- Location: specs/epic-1-visibility.spec.js:76:5
# Error details
```
Test timeout of 120000ms exceeded while running "beforeEach" hook.
```
```
Error: page.waitForFunction: Test timeout of 120000ms exceeded.
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- list:
- listitem [ref=e2]:
- text:
- paragraph [ref=e3]: Foundry Virtual Tabletop nécessite une résolution d'écran de 1366px par 768px ou plus. Votre écran a une résolution de 1280px par 720px. Vous devez augmenter votre résolution ou utiliser un autre périphérique d'affichage, sinon certaines fonctionnalités du logiciel ne fonctionneront pas correctement.
- text:
- banner [ref=e5]:
- heading "Donjon & Cie" [level=1] [ref=e6]
- generic [ref=e7]:
- generic [ref=e8]:
- heading "Rejoindre la partie" [level=2] [ref=e9]
- combobox [ref=e14] [cursor=pointer]:
- option [selected]
- option "Gamemaster" [disabled]
- textbox "Mot de passe" [ref=e19]
- contentinfo [ref=e20]:
- button "Rejoindre la partie" [ref=e21] [cursor=pointer]:
- generic: Rejoindre la partie
- generic [ref=e22]:
- heading "Détails de la session" [level=2] [ref=e23]
- generic [ref=e25]: Prochaine partie
- generic [ref=e27]:
- generic [ref=e28]: Joueurs présents
- generic [ref=e30]:
- generic [ref=e31]: "1"
- generic [ref=e32]: /
- generic [ref=e33]: "1"
- generic [ref=e34]:
- heading "Retour à laccueil" [level=2] [ref=e35]
- textbox [ref=e39]
- contentinfo [ref=e40]:
- button "Retour à laccueil" [ref=e41] [cursor=pointer]:
- generic: Retour à laccueil
- article [ref=e42]:
- heading "Description du monde" [level=2] [ref=e43]
- contentinfo [ref=e44]:
- paragraph [ref=e45]: Version 14 Build 361
```
# Test source
```ts
1 | /**
2 | * Helpers pour les tests E2E avec FoundryVTT
3 | *
4 | * Fournit des fonctions utilitaires pour :
5 | * - Attendre que Foundry soit prêt
6 | * - Attendre que le module soit chargé
7 | * - Interagir avec l'UI FoundryVTT
8 | * - Gérer les sélecteurs spécifiques au module
9 | */
10 |
11 | import { expect } from '@playwright/test';
12 |
13 | /**
14 | * Attend que FoundryVTT soit complètement chargé
15 | * @param {import('@playwright/test').Page} page - La page Playwright
16 | * @param {number} timeout - Timeout en ms (défaut: 30000)
17 | */
18 | export async function waitForFoundryReady(page, timeout = 30000) {
> 19 | await page.waitForFunction(() => {
| ^ Error: page.waitForFunction: Test timeout of 120000ms exceeded.
20 | return typeof game !== 'undefined' && game.ready;
21 | }, { timeout });
22 | }
23 |
24 | /**
25 | * Attend que le module Video View Manager soit actif
26 | * @param {import('@playwright/test').Page} page - La page Playwright
27 | * @param {number} timeout - Timeout en ms (défaut: 15000)
28 | */
29 | export async function waitForVVMModule(page, timeout = 15000) {
30 | await page.waitForFunction(() => {
31 | const module = game.modules?.get?.('video-view-manager');
32 | return module?.active === true;
33 | }, { timeout });
34 | }
35 |
36 | /**
37 | * Attend qu'un élément du module soit présent
38 | * @param {import('@playwright/test').Page} page - La page Playwright
39 | * @param {string} selector - Sélecteur CSS
40 | * @param {number} timeout - Timeout en ms (défaut: 10000)
41 | */
42 | export async function waitForVVMElement(page, selector, timeout = 10000) {
43 | await page.waitForSelector(selector, {
44 | state: 'visible',
45 | timeout
46 | });
47 | }
48 |
49 | /**
50 | * Clique sur un bouton dans l'UI Foundry avec retry
51 | * @param {import('@playwright/test').Page} page - La page Playwright
52 | * @param {string|import('@playwright/test').Locator} button - Sélecteur ou Locator
53 | * @param {number} retries - Nombre de tentatives (défaut: 3)
54 | */
55 | export async function clickFoundryButton(page, button, retries = 3) {
56 | for (let i = 0; i < retries; i++) {
57 | try {
58 | const locator = typeof button === 'string' ? page.locator(button) : button;
59 | await locator.click({ timeout: 5000 });
60 | return;
61 | } catch (error) {
62 | if (i === retries - 1) throw error;
63 | await page.waitForTimeout(1000);
64 | }
65 | }
66 | }
67 |
68 | /**
69 | * Ouvre le sidebar de configuration Foundry
70 | * @param {import('@playwright/test').Page} page - La page Playwright
71 | */
72 | export async function openFoundrySidebar(page) {
73 | await clickFoundryButton(page, 'button[aria-label="Configure Settings"]');
74 | await page.waitForSelector('.app-v2.settings', { state: 'visible', timeout: 10000 });
75 | }
76 |
77 | /**
78 | * Ouvre le Director's Board (Epic 2)
79 | * @param {import('@playwright/test').Page} page - La page Playwright
80 | */
81 | export async function openDirectorsBoard(page) {
82 | // Le Director's Board a un bouton dédié dans la sidebar
83 | await page.waitForSelector('button[aria-label*="Director\'s Board"]', { timeout: 10000 });
84 | await clickFoundryButton(page, 'button[aria-label*="Director\'s Board"]');
85 |
86 | // Attendre que le board soit ouvert
87 | await page.waitForSelector('.scrying-pool-directors-board', {
88 | state: 'visible',
89 | timeout: 10000
90 | });
91 | }
92 |
93 | /**
94 | * Ouvre le Player Privacy Panel pour un utilisateur
95 | * @param {import('@playwright/test').Page} page - La page Playwright
96 | * @param {string} userId - L'ID de l'utilisateur
97 | */
98 | export async function openPlayerPrivacyPanel(page, userId) {
99 | // Le panel s'ouvre via les paramètres du module
100 | await openFoundrySidebar(page);
101 |
102 | // Naviguer vers les paramètres du module
103 | await clickFoundryButton(page, 'button:has-text("Video View Manager")');
104 | await page.waitForTimeout(1000);
105 |
106 | // Cliquer sur le bouton Player Privacy
107 | await clickFoundryButton(page, 'button:has-text("Player Privacy")');
108 |
109 | // Attendre le panel
110 | await page.waitForSelector('.sp-player-privacy-panel', {
111 | state: 'visible',
112 | timeout: 10000
113 | });
114 | }
115 |
116 | /**
117 | * Sélectionne un utilisateur dans une liste Foundry
118 | * @param {import('@playwright/test').Page} page - La page Playwright
119 | * @param {string} username - Le nom de l'utilisateur
```
@@ -0,0 +1,187 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: specs/epic-1-visibility.spec.js >> Epic 1: Core Camera Visibility Control >> FR-1: Hide/Show Participant via Context Menu >> GM can show a hidden participant via right-click
- Location: specs/epic-1-visibility.spec.js:76:5
# Error details
```
Test timeout of 120000ms exceeded while running "beforeEach" hook.
```
```
Error: page.waitForFunction: Test timeout of 120000ms exceeded.
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- list:
- listitem [ref=e2]:
- text:
- paragraph [ref=e3]: Foundry Virtual Tabletop nécessite une résolution d'écran de 1366px par 768px ou plus. Votre écran a une résolution de 1280px par 720px. Vous devez augmenter votre résolution ou utiliser un autre périphérique d'affichage, sinon certaines fonctionnalités du logiciel ne fonctionneront pas correctement.
- text:
- banner [ref=e5]:
- heading "Donjon & Cie" [level=1] [ref=e6]
- generic [ref=e7]:
- generic [ref=e8]:
- heading "Rejoindre la partie" [level=2] [ref=e9]
- combobox [ref=e14] [cursor=pointer]:
- option [selected]
- option "Gamemaster" [disabled]
- textbox "Mot de passe" [ref=e19]
- contentinfo [ref=e20]:
- button "Rejoindre la partie" [ref=e21] [cursor=pointer]:
- generic: Rejoindre la partie
- generic [ref=e22]:
- heading "Détails de la session" [level=2] [ref=e23]
- generic [ref=e25]: Prochaine partie
- generic [ref=e27]:
- generic [ref=e28]: Joueurs présents
- generic [ref=e30]:
- generic [ref=e31]: "1"
- generic [ref=e32]: /
- generic [ref=e33]: "1"
- generic [ref=e34]:
- heading "Retour à laccueil" [level=2] [ref=e35]
- textbox [ref=e39]
- contentinfo [ref=e40]:
- button "Retour à laccueil" [ref=e41] [cursor=pointer]:
- generic: Retour à laccueil
- article [ref=e42]:
- heading "Description du monde" [level=2] [ref=e43]
- contentinfo [ref=e44]:
- paragraph [ref=e45]: Version 14 Build 361
```
# Test source
```ts
1 | /**
2 | * Helpers pour les tests E2E avec FoundryVTT
3 | *
4 | * Fournit des fonctions utilitaires pour :
5 | * - Attendre que Foundry soit prêt
6 | * - Attendre que le module soit chargé
7 | * - Interagir avec l'UI FoundryVTT
8 | * - Gérer les sélecteurs spécifiques au module
9 | */
10 |
11 | import { expect } from '@playwright/test';
12 |
13 | /**
14 | * Attend que FoundryVTT soit complètement chargé
15 | * @param {import('@playwright/test').Page} page - La page Playwright
16 | * @param {number} timeout - Timeout en ms (défaut: 30000)
17 | */
18 | export async function waitForFoundryReady(page, timeout = 30000) {
> 19 | await page.waitForFunction(() => {
| ^ Error: page.waitForFunction: Test timeout of 120000ms exceeded.
20 | return typeof game !== 'undefined' && game.ready;
21 | }, { timeout });
22 | }
23 |
24 | /**
25 | * Attend que le module Video View Manager soit actif
26 | * @param {import('@playwright/test').Page} page - La page Playwright
27 | * @param {number} timeout - Timeout en ms (défaut: 15000)
28 | */
29 | export async function waitForVVMModule(page, timeout = 15000) {
30 | await page.waitForFunction(() => {
31 | const module = game.modules?.get?.('video-view-manager');
32 | return module?.active === true;
33 | }, { timeout });
34 | }
35 |
36 | /**
37 | * Attend qu'un élément du module soit présent
38 | * @param {import('@playwright/test').Page} page - La page Playwright
39 | * @param {string} selector - Sélecteur CSS
40 | * @param {number} timeout - Timeout en ms (défaut: 10000)
41 | */
42 | export async function waitForVVMElement(page, selector, timeout = 10000) {
43 | await page.waitForSelector(selector, {
44 | state: 'visible',
45 | timeout
46 | });
47 | }
48 |
49 | /**
50 | * Clique sur un bouton dans l'UI Foundry avec retry
51 | * @param {import('@playwright/test').Page} page - La page Playwright
52 | * @param {string|import('@playwright/test').Locator} button - Sélecteur ou Locator
53 | * @param {number} retries - Nombre de tentatives (défaut: 3)
54 | */
55 | export async function clickFoundryButton(page, button, retries = 3) {
56 | for (let i = 0; i < retries; i++) {
57 | try {
58 | const locator = typeof button === 'string' ? page.locator(button) : button;
59 | await locator.click({ timeout: 5000 });
60 | return;
61 | } catch (error) {
62 | if (i === retries - 1) throw error;
63 | await page.waitForTimeout(1000);
64 | }
65 | }
66 | }
67 |
68 | /**
69 | * Ouvre le sidebar de configuration Foundry
70 | * @param {import('@playwright/test').Page} page - La page Playwright
71 | */
72 | export async function openFoundrySidebar(page) {
73 | await clickFoundryButton(page, 'button[aria-label="Configure Settings"]');
74 | await page.waitForSelector('.app-v2.settings', { state: 'visible', timeout: 10000 });
75 | }
76 |
77 | /**
78 | * Ouvre le Director's Board (Epic 2)
79 | * @param {import('@playwright/test').Page} page - La page Playwright
80 | */
81 | export async function openDirectorsBoard(page) {
82 | // Le Director's Board a un bouton dédié dans la sidebar
83 | await page.waitForSelector('button[aria-label*="Director\'s Board"]', { timeout: 10000 });
84 | await clickFoundryButton(page, 'button[aria-label*="Director\'s Board"]');
85 |
86 | // Attendre que le board soit ouvert
87 | await page.waitForSelector('.scrying-pool-directors-board', {
88 | state: 'visible',
89 | timeout: 10000
90 | });
91 | }
92 |
93 | /**
94 | * Ouvre le Player Privacy Panel pour un utilisateur
95 | * @param {import('@playwright/test').Page} page - La page Playwright
96 | * @param {string} userId - L'ID de l'utilisateur
97 | */
98 | export async function openPlayerPrivacyPanel(page, userId) {
99 | // Le panel s'ouvre via les paramètres du module
100 | await openFoundrySidebar(page);
101 |
102 | // Naviguer vers les paramètres du module
103 | await clickFoundryButton(page, 'button:has-text("Video View Manager")');
104 | await page.waitForTimeout(1000);
105 |
106 | // Cliquer sur le bouton Player Privacy
107 | await clickFoundryButton(page, 'button:has-text("Player Privacy")');
108 |
109 | // Attendre le panel
110 | await page.waitForSelector('.sp-player-privacy-panel', {
111 | state: 'visible',
112 | timeout: 10000
113 | });
114 | }
115 |
116 | /**
117 | * Sélectionne un utilisateur dans une liste Foundry
118 | * @param {import('@playwright/test').Page} page - La page Playwright
119 | * @param {string} username - Le nom de l'utilisateur
```
@@ -0,0 +1,187 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: specs/epic-1-visibility.spec.js >> Epic 1: Core Camera Visibility Control >> FR-1: Hide/Show Participant via Context Menu >> GM can show a hidden participant via right-click
- Location: specs/epic-1-visibility.spec.js:76:5
# Error details
```
Test timeout of 120000ms exceeded while running "beforeEach" hook.
```
```
Error: page.waitForFunction: Test timeout of 120000ms exceeded.
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- list:
- listitem [ref=e2]:
- text:
- paragraph [ref=e3]: Foundry Virtual Tabletop nécessite une résolution d'écran de 1366px par 768px ou plus. Votre écran a une résolution de 1280px par 720px. Vous devez augmenter votre résolution ou utiliser un autre périphérique d'affichage, sinon certaines fonctionnalités du logiciel ne fonctionneront pas correctement.
- text:
- banner [ref=e5]:
- heading "Donjon & Cie" [level=1] [ref=e6]
- generic [ref=e7]:
- generic [ref=e8]:
- heading "Rejoindre la partie" [level=2] [ref=e9]
- combobox [ref=e14] [cursor=pointer]:
- option [selected]
- option "Gamemaster" [disabled]
- textbox "Mot de passe" [ref=e19]
- contentinfo [ref=e20]:
- button "Rejoindre la partie" [ref=e21] [cursor=pointer]:
- generic: Rejoindre la partie
- generic [ref=e22]:
- heading "Détails de la session" [level=2] [ref=e23]
- generic [ref=e25]: Prochaine partie
- generic [ref=e27]:
- generic [ref=e28]: Joueurs présents
- generic [ref=e30]:
- generic [ref=e31]: "1"
- generic [ref=e32]: /
- generic [ref=e33]: "1"
- generic [ref=e34]:
- heading "Retour à laccueil" [level=2] [ref=e35]
- textbox [ref=e39]
- contentinfo [ref=e40]:
- button "Retour à laccueil" [ref=e41] [cursor=pointer]:
- generic: Retour à laccueil
- article [ref=e42]:
- heading "Description du monde" [level=2] [ref=e43]
- contentinfo [ref=e44]:
- paragraph [ref=e45]: Version 14 Build 361
```
# Test source
```ts
1 | /**
2 | * Helpers pour les tests E2E avec FoundryVTT
3 | *
4 | * Fournit des fonctions utilitaires pour :
5 | * - Attendre que Foundry soit prêt
6 | * - Attendre que le module soit chargé
7 | * - Interagir avec l'UI FoundryVTT
8 | * - Gérer les sélecteurs spécifiques au module
9 | */
10 |
11 | import { expect } from '@playwright/test';
12 |
13 | /**
14 | * Attend que FoundryVTT soit complètement chargé
15 | * @param {import('@playwright/test').Page} page - La page Playwright
16 | * @param {number} timeout - Timeout en ms (défaut: 30000)
17 | */
18 | export async function waitForFoundryReady(page, timeout = 30000) {
> 19 | await page.waitForFunction(() => {
| ^ Error: page.waitForFunction: Test timeout of 120000ms exceeded.
20 | return typeof game !== 'undefined' && game.ready;
21 | }, { timeout });
22 | }
23 |
24 | /**
25 | * Attend que le module Video View Manager soit actif
26 | * @param {import('@playwright/test').Page} page - La page Playwright
27 | * @param {number} timeout - Timeout en ms (défaut: 15000)
28 | */
29 | export async function waitForVVMModule(page, timeout = 15000) {
30 | await page.waitForFunction(() => {
31 | const module = game.modules?.get?.('video-view-manager');
32 | return module?.active === true;
33 | }, { timeout });
34 | }
35 |
36 | /**
37 | * Attend qu'un élément du module soit présent
38 | * @param {import('@playwright/test').Page} page - La page Playwright
39 | * @param {string} selector - Sélecteur CSS
40 | * @param {number} timeout - Timeout en ms (défaut: 10000)
41 | */
42 | export async function waitForVVMElement(page, selector, timeout = 10000) {
43 | await page.waitForSelector(selector, {
44 | state: 'visible',
45 | timeout
46 | });
47 | }
48 |
49 | /**
50 | * Clique sur un bouton dans l'UI Foundry avec retry
51 | * @param {import('@playwright/test').Page} page - La page Playwright
52 | * @param {string|import('@playwright/test').Locator} button - Sélecteur ou Locator
53 | * @param {number} retries - Nombre de tentatives (défaut: 3)
54 | */
55 | export async function clickFoundryButton(page, button, retries = 3) {
56 | for (let i = 0; i < retries; i++) {
57 | try {
58 | const locator = typeof button === 'string' ? page.locator(button) : button;
59 | await locator.click({ timeout: 5000 });
60 | return;
61 | } catch (error) {
62 | if (i === retries - 1) throw error;
63 | await page.waitForTimeout(1000);
64 | }
65 | }
66 | }
67 |
68 | /**
69 | * Ouvre le sidebar de configuration Foundry
70 | * @param {import('@playwright/test').Page} page - La page Playwright
71 | */
72 | export async function openFoundrySidebar(page) {
73 | await clickFoundryButton(page, 'button[aria-label="Configure Settings"]');
74 | await page.waitForSelector('.app-v2.settings', { state: 'visible', timeout: 10000 });
75 | }
76 |
77 | /**
78 | * Ouvre le Director's Board (Epic 2)
79 | * @param {import('@playwright/test').Page} page - La page Playwright
80 | */
81 | export async function openDirectorsBoard(page) {
82 | // Le Director's Board a un bouton dédié dans la sidebar
83 | await page.waitForSelector('button[aria-label*="Director\'s Board"]', { timeout: 10000 });
84 | await clickFoundryButton(page, 'button[aria-label*="Director\'s Board"]');
85 |
86 | // Attendre que le board soit ouvert
87 | await page.waitForSelector('.scrying-pool-directors-board', {
88 | state: 'visible',
89 | timeout: 10000
90 | });
91 | }
92 |
93 | /**
94 | * Ouvre le Player Privacy Panel pour un utilisateur
95 | * @param {import('@playwright/test').Page} page - La page Playwright
96 | * @param {string} userId - L'ID de l'utilisateur
97 | */
98 | export async function openPlayerPrivacyPanel(page, userId) {
99 | // Le panel s'ouvre via les paramètres du module
100 | await openFoundrySidebar(page);
101 |
102 | // Naviguer vers les paramètres du module
103 | await clickFoundryButton(page, 'button:has-text("Video View Manager")');
104 | await page.waitForTimeout(1000);
105 |
106 | // Cliquer sur le bouton Player Privacy
107 | await clickFoundryButton(page, 'button:has-text("Player Privacy")');
108 |
109 | // Attendre le panel
110 | await page.waitForSelector('.sp-player-privacy-panel', {
111 | state: 'visible',
112 | timeout: 10000
113 | });
114 | }
115 |
116 | /**
117 | * Sélectionne un utilisateur dans une liste Foundry
118 | * @param {import('@playwright/test').Page} page - La page Playwright
119 | * @param {string} username - Le nom de l'utilisateur
```
@@ -0,0 +1,187 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: specs/epic-1-visibility.spec.js >> Epic 1: Core Camera Visibility Control >> FR-1: Hide/Show Participant via Context Menu >> Visibility change is immediate (no layout shift)
- Location: specs/epic-1-visibility.spec.js:92:5
# Error details
```
Test timeout of 120000ms exceeded while running "beforeEach" hook.
```
```
Error: page.waitForFunction: Test timeout of 120000ms exceeded.
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- list:
- listitem [ref=e2]:
- text:
- paragraph [ref=e3]: Foundry Virtual Tabletop nécessite une résolution d'écran de 1366px par 768px ou plus. Votre écran a une résolution de 1280px par 720px. Vous devez augmenter votre résolution ou utiliser un autre périphérique d'affichage, sinon certaines fonctionnalités du logiciel ne fonctionneront pas correctement.
- text:
- banner [ref=e5]:
- heading "Donjon & Cie" [level=1] [ref=e6]
- generic [ref=e7]:
- generic [ref=e8]:
- heading "Rejoindre la partie" [level=2] [ref=e9]
- combobox [ref=e14] [cursor=pointer]:
- option [selected]
- option "Gamemaster" [disabled]
- textbox "Mot de passe" [ref=e19]
- contentinfo [ref=e20]:
- button "Rejoindre la partie" [ref=e21] [cursor=pointer]:
- generic: Rejoindre la partie
- generic [ref=e22]:
- heading "Détails de la session" [level=2] [ref=e23]
- generic [ref=e25]: Prochaine partie
- generic [ref=e27]:
- generic [ref=e28]: Joueurs présents
- generic [ref=e30]:
- generic [ref=e31]: "1"
- generic [ref=e32]: /
- generic [ref=e33]: "1"
- generic [ref=e34]:
- heading "Retour à laccueil" [level=2] [ref=e35]
- textbox [ref=e39]
- contentinfo [ref=e40]:
- button "Retour à laccueil" [ref=e41] [cursor=pointer]:
- generic: Retour à laccueil
- article [ref=e42]:
- heading "Description du monde" [level=2] [ref=e43]
- contentinfo [ref=e44]:
- paragraph [ref=e45]: Version 14 Build 361
```
# Test source
```ts
1 | /**
2 | * Helpers pour les tests E2E avec FoundryVTT
3 | *
4 | * Fournit des fonctions utilitaires pour :
5 | * - Attendre que Foundry soit prêt
6 | * - Attendre que le module soit chargé
7 | * - Interagir avec l'UI FoundryVTT
8 | * - Gérer les sélecteurs spécifiques au module
9 | */
10 |
11 | import { expect } from '@playwright/test';
12 |
13 | /**
14 | * Attend que FoundryVTT soit complètement chargé
15 | * @param {import('@playwright/test').Page} page - La page Playwright
16 | * @param {number} timeout - Timeout en ms (défaut: 30000)
17 | */
18 | export async function waitForFoundryReady(page, timeout = 30000) {
> 19 | await page.waitForFunction(() => {
| ^ Error: page.waitForFunction: Test timeout of 120000ms exceeded.
20 | return typeof game !== 'undefined' && game.ready;
21 | }, { timeout });
22 | }
23 |
24 | /**
25 | * Attend que le module Video View Manager soit actif
26 | * @param {import('@playwright/test').Page} page - La page Playwright
27 | * @param {number} timeout - Timeout en ms (défaut: 15000)
28 | */
29 | export async function waitForVVMModule(page, timeout = 15000) {
30 | await page.waitForFunction(() => {
31 | const module = game.modules?.get?.('video-view-manager');
32 | return module?.active === true;
33 | }, { timeout });
34 | }
35 |
36 | /**
37 | * Attend qu'un élément du module soit présent
38 | * @param {import('@playwright/test').Page} page - La page Playwright
39 | * @param {string} selector - Sélecteur CSS
40 | * @param {number} timeout - Timeout en ms (défaut: 10000)
41 | */
42 | export async function waitForVVMElement(page, selector, timeout = 10000) {
43 | await page.waitForSelector(selector, {
44 | state: 'visible',
45 | timeout
46 | });
47 | }
48 |
49 | /**
50 | * Clique sur un bouton dans l'UI Foundry avec retry
51 | * @param {import('@playwright/test').Page} page - La page Playwright
52 | * @param {string|import('@playwright/test').Locator} button - Sélecteur ou Locator
53 | * @param {number} retries - Nombre de tentatives (défaut: 3)
54 | */
55 | export async function clickFoundryButton(page, button, retries = 3) {
56 | for (let i = 0; i < retries; i++) {
57 | try {
58 | const locator = typeof button === 'string' ? page.locator(button) : button;
59 | await locator.click({ timeout: 5000 });
60 | return;
61 | } catch (error) {
62 | if (i === retries - 1) throw error;
63 | await page.waitForTimeout(1000);
64 | }
65 | }
66 | }
67 |
68 | /**
69 | * Ouvre le sidebar de configuration Foundry
70 | * @param {import('@playwright/test').Page} page - La page Playwright
71 | */
72 | export async function openFoundrySidebar(page) {
73 | await clickFoundryButton(page, 'button[aria-label="Configure Settings"]');
74 | await page.waitForSelector('.app-v2.settings', { state: 'visible', timeout: 10000 });
75 | }
76 |
77 | /**
78 | * Ouvre le Director's Board (Epic 2)
79 | * @param {import('@playwright/test').Page} page - La page Playwright
80 | */
81 | export async function openDirectorsBoard(page) {
82 | // Le Director's Board a un bouton dédié dans la sidebar
83 | await page.waitForSelector('button[aria-label*="Director\'s Board"]', { timeout: 10000 });
84 | await clickFoundryButton(page, 'button[aria-label*="Director\'s Board"]');
85 |
86 | // Attendre que le board soit ouvert
87 | await page.waitForSelector('.scrying-pool-directors-board', {
88 | state: 'visible',
89 | timeout: 10000
90 | });
91 | }
92 |
93 | /**
94 | * Ouvre le Player Privacy Panel pour un utilisateur
95 | * @param {import('@playwright/test').Page} page - La page Playwright
96 | * @param {string} userId - L'ID de l'utilisateur
97 | */
98 | export async function openPlayerPrivacyPanel(page, userId) {
99 | // Le panel s'ouvre via les paramètres du module
100 | await openFoundrySidebar(page);
101 |
102 | // Naviguer vers les paramètres du module
103 | await clickFoundryButton(page, 'button:has-text("Video View Manager")');
104 | await page.waitForTimeout(1000);
105 |
106 | // Cliquer sur le bouton Player Privacy
107 | await clickFoundryButton(page, 'button:has-text("Player Privacy")');
108 |
109 | // Attendre le panel
110 | await page.waitForSelector('.sp-player-privacy-panel', {
111 | state: 'visible',
112 | timeout: 10000
113 | });
114 | }
115 |
116 | /**
117 | * Sélectionne un utilisateur dans une liste Foundry
118 | * @param {import('@playwright/test').Page} page - La page Playwright
119 | * @param {string} username - Le nom de l'utilisateur
```
@@ -0,0 +1,187 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: specs/epic-1-visibility.spec.js >> Epic 1: Core Camera Visibility Control >> FR-1: Hide/Show Participant via Context Menu >> Visibility change is immediate (no layout shift)
- Location: specs/epic-1-visibility.spec.js:92:5
# Error details
```
Test timeout of 120000ms exceeded while running "beforeEach" hook.
```
```
Error: page.waitForFunction: Test timeout of 120000ms exceeded.
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- list:
- listitem [ref=e2]:
- text:
- paragraph [ref=e3]: Foundry Virtual Tabletop nécessite une résolution d'écran de 1366px par 768px ou plus. Votre écran a une résolution de 1280px par 720px. Vous devez augmenter votre résolution ou utiliser un autre périphérique d'affichage, sinon certaines fonctionnalités du logiciel ne fonctionneront pas correctement.
- text:
- banner [ref=e5]:
- heading "Donjon & Cie" [level=1] [ref=e6]
- generic [ref=e7]:
- generic [ref=e8]:
- heading "Rejoindre la partie" [level=2] [ref=e9]
- combobox [ref=e14] [cursor=pointer]:
- option [selected]
- option "Gamemaster" [disabled]
- textbox "Mot de passe" [ref=e19]
- contentinfo [ref=e20]:
- button "Rejoindre la partie" [ref=e21] [cursor=pointer]:
- generic: Rejoindre la partie
- generic [ref=e22]:
- heading "Détails de la session" [level=2] [ref=e23]
- generic [ref=e25]: Prochaine partie
- generic [ref=e27]:
- generic [ref=e28]: Joueurs présents
- generic [ref=e30]:
- generic [ref=e31]: "1"
- generic [ref=e32]: /
- generic [ref=e33]: "1"
- generic [ref=e34]:
- heading "Retour à laccueil" [level=2] [ref=e35]
- textbox [ref=e39]
- contentinfo [ref=e40]:
- button "Retour à laccueil" [ref=e41] [cursor=pointer]:
- generic: Retour à laccueil
- article [ref=e42]:
- heading "Description du monde" [level=2] [ref=e43]
- contentinfo [ref=e44]:
- paragraph [ref=e45]: Version 14 Build 361
```
# Test source
```ts
1 | /**
2 | * Helpers pour les tests E2E avec FoundryVTT
3 | *
4 | * Fournit des fonctions utilitaires pour :
5 | * - Attendre que Foundry soit prêt
6 | * - Attendre que le module soit chargé
7 | * - Interagir avec l'UI FoundryVTT
8 | * - Gérer les sélecteurs spécifiques au module
9 | */
10 |
11 | import { expect } from '@playwright/test';
12 |
13 | /**
14 | * Attend que FoundryVTT soit complètement chargé
15 | * @param {import('@playwright/test').Page} page - La page Playwright
16 | * @param {number} timeout - Timeout en ms (défaut: 30000)
17 | */
18 | export async function waitForFoundryReady(page, timeout = 30000) {
> 19 | await page.waitForFunction(() => {
| ^ Error: page.waitForFunction: Test timeout of 120000ms exceeded.
20 | return typeof game !== 'undefined' && game.ready;
21 | }, { timeout });
22 | }
23 |
24 | /**
25 | * Attend que le module Video View Manager soit actif
26 | * @param {import('@playwright/test').Page} page - La page Playwright
27 | * @param {number} timeout - Timeout en ms (défaut: 15000)
28 | */
29 | export async function waitForVVMModule(page, timeout = 15000) {
30 | await page.waitForFunction(() => {
31 | const module = game.modules?.get?.('video-view-manager');
32 | return module?.active === true;
33 | }, { timeout });
34 | }
35 |
36 | /**
37 | * Attend qu'un élément du module soit présent
38 | * @param {import('@playwright/test').Page} page - La page Playwright
39 | * @param {string} selector - Sélecteur CSS
40 | * @param {number} timeout - Timeout en ms (défaut: 10000)
41 | */
42 | export async function waitForVVMElement(page, selector, timeout = 10000) {
43 | await page.waitForSelector(selector, {
44 | state: 'visible',
45 | timeout
46 | });
47 | }
48 |
49 | /**
50 | * Clique sur un bouton dans l'UI Foundry avec retry
51 | * @param {import('@playwright/test').Page} page - La page Playwright
52 | * @param {string|import('@playwright/test').Locator} button - Sélecteur ou Locator
53 | * @param {number} retries - Nombre de tentatives (défaut: 3)
54 | */
55 | export async function clickFoundryButton(page, button, retries = 3) {
56 | for (let i = 0; i < retries; i++) {
57 | try {
58 | const locator = typeof button === 'string' ? page.locator(button) : button;
59 | await locator.click({ timeout: 5000 });
60 | return;
61 | } catch (error) {
62 | if (i === retries - 1) throw error;
63 | await page.waitForTimeout(1000);
64 | }
65 | }
66 | }
67 |
68 | /**
69 | * Ouvre le sidebar de configuration Foundry
70 | * @param {import('@playwright/test').Page} page - La page Playwright
71 | */
72 | export async function openFoundrySidebar(page) {
73 | await clickFoundryButton(page, 'button[aria-label="Configure Settings"]');
74 | await page.waitForSelector('.app-v2.settings', { state: 'visible', timeout: 10000 });
75 | }
76 |
77 | /**
78 | * Ouvre le Director's Board (Epic 2)
79 | * @param {import('@playwright/test').Page} page - La page Playwright
80 | */
81 | export async function openDirectorsBoard(page) {
82 | // Le Director's Board a un bouton dédié dans la sidebar
83 | await page.waitForSelector('button[aria-label*="Director\'s Board"]', { timeout: 10000 });
84 | await clickFoundryButton(page, 'button[aria-label*="Director\'s Board"]');
85 |
86 | // Attendre que le board soit ouvert
87 | await page.waitForSelector('.scrying-pool-directors-board', {
88 | state: 'visible',
89 | timeout: 10000
90 | });
91 | }
92 |
93 | /**
94 | * Ouvre le Player Privacy Panel pour un utilisateur
95 | * @param {import('@playwright/test').Page} page - La page Playwright
96 | * @param {string} userId - L'ID de l'utilisateur
97 | */
98 | export async function openPlayerPrivacyPanel(page, userId) {
99 | // Le panel s'ouvre via les paramètres du module
100 | await openFoundrySidebar(page);
101 |
102 | // Naviguer vers les paramètres du module
103 | await clickFoundryButton(page, 'button:has-text("Video View Manager")');
104 | await page.waitForTimeout(1000);
105 |
106 | // Cliquer sur le bouton Player Privacy
107 | await clickFoundryButton(page, 'button:has-text("Player Privacy")');
108 |
109 | // Attendre le panel
110 | await page.waitForSelector('.sp-player-privacy-panel', {
111 | state: 'visible',
112 | timeout: 10000
113 | });
114 | }
115 |
116 | /**
117 | * Sélectionne un utilisateur dans une liste Foundry
118 | * @param {import('@playwright/test').Page} page - La page Playwright
119 | * @param {string} username - Le nom de l'utilisateur
```
@@ -0,0 +1,187 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: specs/epic-1-visibility.spec.js >> Epic 1: Core Camera Visibility Control >> FR-1: Hide/Show Participant via Context Menu >> Visibility change is immediate (no layout shift)
- Location: specs/epic-1-visibility.spec.js:92:5
# Error details
```
Test timeout of 120000ms exceeded while running "beforeEach" hook.
```
```
Error: page.waitForFunction: Test timeout of 120000ms exceeded.
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- list:
- listitem [ref=e2]:
- text:
- paragraph [ref=e3]: Foundry Virtual Tabletop nécessite une résolution d'écran de 1366px par 768px ou plus. Votre écran a une résolution de 1280px par 720px. Vous devez augmenter votre résolution ou utiliser un autre périphérique d'affichage, sinon certaines fonctionnalités du logiciel ne fonctionneront pas correctement.
- text:
- banner [ref=e5]:
- heading "Donjon & Cie" [level=1] [ref=e6]
- generic [ref=e7]:
- generic [ref=e8]:
- heading "Rejoindre la partie" [level=2] [ref=e9]
- combobox [ref=e14] [cursor=pointer]:
- option [selected]
- option "Gamemaster" [disabled]
- textbox "Mot de passe" [ref=e19]
- contentinfo [ref=e20]:
- button "Rejoindre la partie" [ref=e21] [cursor=pointer]:
- generic: Rejoindre la partie
- generic [ref=e22]:
- heading "Détails de la session" [level=2] [ref=e23]
- generic [ref=e25]: Prochaine partie
- generic [ref=e27]:
- generic [ref=e28]: Joueurs présents
- generic [ref=e30]:
- generic [ref=e31]: "1"
- generic [ref=e32]: /
- generic [ref=e33]: "1"
- generic [ref=e34]:
- heading "Retour à laccueil" [level=2] [ref=e35]
- textbox [ref=e39]
- contentinfo [ref=e40]:
- button "Retour à laccueil" [ref=e41] [cursor=pointer]:
- generic: Retour à laccueil
- article [ref=e42]:
- heading "Description du monde" [level=2] [ref=e43]
- contentinfo [ref=e44]:
- paragraph [ref=e45]: Version 14 Build 361
```
# Test source
```ts
1 | /**
2 | * Helpers pour les tests E2E avec FoundryVTT
3 | *
4 | * Fournit des fonctions utilitaires pour :
5 | * - Attendre que Foundry soit prêt
6 | * - Attendre que le module soit chargé
7 | * - Interagir avec l'UI FoundryVTT
8 | * - Gérer les sélecteurs spécifiques au module
9 | */
10 |
11 | import { expect } from '@playwright/test';
12 |
13 | /**
14 | * Attend que FoundryVTT soit complètement chargé
15 | * @param {import('@playwright/test').Page} page - La page Playwright
16 | * @param {number} timeout - Timeout en ms (défaut: 30000)
17 | */
18 | export async function waitForFoundryReady(page, timeout = 30000) {
> 19 | await page.waitForFunction(() => {
| ^ Error: page.waitForFunction: Test timeout of 120000ms exceeded.
20 | return typeof game !== 'undefined' && game.ready;
21 | }, { timeout });
22 | }
23 |
24 | /**
25 | * Attend que le module Video View Manager soit actif
26 | * @param {import('@playwright/test').Page} page - La page Playwright
27 | * @param {number} timeout - Timeout en ms (défaut: 15000)
28 | */
29 | export async function waitForVVMModule(page, timeout = 15000) {
30 | await page.waitForFunction(() => {
31 | const module = game.modules?.get?.('video-view-manager');
32 | return module?.active === true;
33 | }, { timeout });
34 | }
35 |
36 | /**
37 | * Attend qu'un élément du module soit présent
38 | * @param {import('@playwright/test').Page} page - La page Playwright
39 | * @param {string} selector - Sélecteur CSS
40 | * @param {number} timeout - Timeout en ms (défaut: 10000)
41 | */
42 | export async function waitForVVMElement(page, selector, timeout = 10000) {
43 | await page.waitForSelector(selector, {
44 | state: 'visible',
45 | timeout
46 | });
47 | }
48 |
49 | /**
50 | * Clique sur un bouton dans l'UI Foundry avec retry
51 | * @param {import('@playwright/test').Page} page - La page Playwright
52 | * @param {string|import('@playwright/test').Locator} button - Sélecteur ou Locator
53 | * @param {number} retries - Nombre de tentatives (défaut: 3)
54 | */
55 | export async function clickFoundryButton(page, button, retries = 3) {
56 | for (let i = 0; i < retries; i++) {
57 | try {
58 | const locator = typeof button === 'string' ? page.locator(button) : button;
59 | await locator.click({ timeout: 5000 });
60 | return;
61 | } catch (error) {
62 | if (i === retries - 1) throw error;
63 | await page.waitForTimeout(1000);
64 | }
65 | }
66 | }
67 |
68 | /**
69 | * Ouvre le sidebar de configuration Foundry
70 | * @param {import('@playwright/test').Page} page - La page Playwright
71 | */
72 | export async function openFoundrySidebar(page) {
73 | await clickFoundryButton(page, 'button[aria-label="Configure Settings"]');
74 | await page.waitForSelector('.app-v2.settings', { state: 'visible', timeout: 10000 });
75 | }
76 |
77 | /**
78 | * Ouvre le Director's Board (Epic 2)
79 | * @param {import('@playwright/test').Page} page - La page Playwright
80 | */
81 | export async function openDirectorsBoard(page) {
82 | // Le Director's Board a un bouton dédié dans la sidebar
83 | await page.waitForSelector('button[aria-label*="Director\'s Board"]', { timeout: 10000 });
84 | await clickFoundryButton(page, 'button[aria-label*="Director\'s Board"]');
85 |
86 | // Attendre que le board soit ouvert
87 | await page.waitForSelector('.scrying-pool-directors-board', {
88 | state: 'visible',
89 | timeout: 10000
90 | });
91 | }
92 |
93 | /**
94 | * Ouvre le Player Privacy Panel pour un utilisateur
95 | * @param {import('@playwright/test').Page} page - La page Playwright
96 | * @param {string} userId - L'ID de l'utilisateur
97 | */
98 | export async function openPlayerPrivacyPanel(page, userId) {
99 | // Le panel s'ouvre via les paramètres du module
100 | await openFoundrySidebar(page);
101 |
102 | // Naviguer vers les paramètres du module
103 | await clickFoundryButton(page, 'button:has-text("Video View Manager")');
104 | await page.waitForTimeout(1000);
105 |
106 | // Cliquer sur le bouton Player Privacy
107 | await clickFoundryButton(page, 'button:has-text("Player Privacy")');
108 |
109 | // Attendre le panel
110 | await page.waitForSelector('.sp-player-privacy-panel', {
111 | state: 'visible',
112 | timeout: 10000
113 | });
114 | }
115 |
116 | /**
117 | * Sélectionne un utilisateur dans une liste Foundry
118 | * @param {import('@playwright/test').Page} page - La page Playwright
119 | * @param {string} username - Le nom de l'utilisateur
```
@@ -0,0 +1,187 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: specs/epic-1-visibility.spec.js >> Epic 1: Core Camera Visibility Control >> FR-1: Hide/Show Participant via Context Menu >> GM can hide a participant from the table via right-click
- Location: specs/epic-1-visibility.spec.js:60:5
# Error details
```
Test timeout of 120000ms exceeded while running "beforeEach" hook.
```
```
Error: page.waitForFunction: Test timeout of 120000ms exceeded.
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- list:
- listitem [ref=e2]:
- text:
- paragraph [ref=e3]: Foundry Virtual Tabletop nécessite une résolution d'écran de 1366px par 768px ou plus. Votre écran a une résolution de 1280px par 720px. Vous devez augmenter votre résolution ou utiliser un autre périphérique d'affichage, sinon certaines fonctionnalités du logiciel ne fonctionneront pas correctement.
- text:
- banner [ref=e5]:
- heading "Donjon & Cie" [level=1] [ref=e6]
- generic [ref=e7]:
- generic [ref=e8]:
- heading "Rejoindre la partie" [level=2] [ref=e9]
- combobox [ref=e14] [cursor=pointer]:
- option [selected]
- option "Gamemaster" [disabled]
- textbox "Mot de passe" [ref=e19]
- contentinfo [ref=e20]:
- button "Rejoindre la partie" [ref=e21] [cursor=pointer]:
- generic: Rejoindre la partie
- generic [ref=e22]:
- heading "Détails de la session" [level=2] [ref=e23]
- generic [ref=e25]: Prochaine partie
- generic [ref=e27]:
- generic [ref=e28]: Joueurs présents
- generic [ref=e30]:
- generic [ref=e31]: "1"
- generic [ref=e32]: /
- generic [ref=e33]: "1"
- generic [ref=e34]:
- heading "Retour à laccueil" [level=2] [ref=e35]
- textbox [ref=e39]
- contentinfo [ref=e40]:
- button "Retour à laccueil" [ref=e41] [cursor=pointer]:
- generic: Retour à laccueil
- article [ref=e42]:
- heading "Description du monde" [level=2] [ref=e43]
- contentinfo [ref=e44]:
- paragraph [ref=e45]: Version 14 Build 361
```
# Test source
```ts
1 | /**
2 | * Helpers pour les tests E2E avec FoundryVTT
3 | *
4 | * Fournit des fonctions utilitaires pour :
5 | * - Attendre que Foundry soit prêt
6 | * - Attendre que le module soit chargé
7 | * - Interagir avec l'UI FoundryVTT
8 | * - Gérer les sélecteurs spécifiques au module
9 | */
10 |
11 | import { expect } from '@playwright/test';
12 |
13 | /**
14 | * Attend que FoundryVTT soit complètement chargé
15 | * @param {import('@playwright/test').Page} page - La page Playwright
16 | * @param {number} timeout - Timeout en ms (défaut: 30000)
17 | */
18 | export async function waitForFoundryReady(page, timeout = 30000) {
> 19 | await page.waitForFunction(() => {
| ^ Error: page.waitForFunction: Test timeout of 120000ms exceeded.
20 | return typeof game !== 'undefined' && game.ready;
21 | }, { timeout });
22 | }
23 |
24 | /**
25 | * Attend que le module Video View Manager soit actif
26 | * @param {import('@playwright/test').Page} page - La page Playwright
27 | * @param {number} timeout - Timeout en ms (défaut: 15000)
28 | */
29 | export async function waitForVVMModule(page, timeout = 15000) {
30 | await page.waitForFunction(() => {
31 | const module = game.modules?.get?.('video-view-manager');
32 | return module?.active === true;
33 | }, { timeout });
34 | }
35 |
36 | /**
37 | * Attend qu'un élément du module soit présent
38 | * @param {import('@playwright/test').Page} page - La page Playwright
39 | * @param {string} selector - Sélecteur CSS
40 | * @param {number} timeout - Timeout en ms (défaut: 10000)
41 | */
42 | export async function waitForVVMElement(page, selector, timeout = 10000) {
43 | await page.waitForSelector(selector, {
44 | state: 'visible',
45 | timeout
46 | });
47 | }
48 |
49 | /**
50 | * Clique sur un bouton dans l'UI Foundry avec retry
51 | * @param {import('@playwright/test').Page} page - La page Playwright
52 | * @param {string|import('@playwright/test').Locator} button - Sélecteur ou Locator
53 | * @param {number} retries - Nombre de tentatives (défaut: 3)
54 | */
55 | export async function clickFoundryButton(page, button, retries = 3) {
56 | for (let i = 0; i < retries; i++) {
57 | try {
58 | const locator = typeof button === 'string' ? page.locator(button) : button;
59 | await locator.click({ timeout: 5000 });
60 | return;
61 | } catch (error) {
62 | if (i === retries - 1) throw error;
63 | await page.waitForTimeout(1000);
64 | }
65 | }
66 | }
67 |
68 | /**
69 | * Ouvre le sidebar de configuration Foundry
70 | * @param {import('@playwright/test').Page} page - La page Playwright
71 | */
72 | export async function openFoundrySidebar(page) {
73 | await clickFoundryButton(page, 'button[aria-label="Configure Settings"]');
74 | await page.waitForSelector('.app-v2.settings', { state: 'visible', timeout: 10000 });
75 | }
76 |
77 | /**
78 | * Ouvre le Director's Board (Epic 2)
79 | * @param {import('@playwright/test').Page} page - La page Playwright
80 | */
81 | export async function openDirectorsBoard(page) {
82 | // Le Director's Board a un bouton dédié dans la sidebar
83 | await page.waitForSelector('button[aria-label*="Director\'s Board"]', { timeout: 10000 });
84 | await clickFoundryButton(page, 'button[aria-label*="Director\'s Board"]');
85 |
86 | // Attendre que le board soit ouvert
87 | await page.waitForSelector('.scrying-pool-directors-board', {
88 | state: 'visible',
89 | timeout: 10000
90 | });
91 | }
92 |
93 | /**
94 | * Ouvre le Player Privacy Panel pour un utilisateur
95 | * @param {import('@playwright/test').Page} page - La page Playwright
96 | * @param {string} userId - L'ID de l'utilisateur
97 | */
98 | export async function openPlayerPrivacyPanel(page, userId) {
99 | // Le panel s'ouvre via les paramètres du module
100 | await openFoundrySidebar(page);
101 |
102 | // Naviguer vers les paramètres du module
103 | await clickFoundryButton(page, 'button:has-text("Video View Manager")');
104 | await page.waitForTimeout(1000);
105 |
106 | // Cliquer sur le bouton Player Privacy
107 | await clickFoundryButton(page, 'button:has-text("Player Privacy")');
108 |
109 | // Attendre le panel
110 | await page.waitForSelector('.sp-player-privacy-panel', {
111 | state: 'visible',
112 | timeout: 10000
113 | });
114 | }
115 |
116 | /**
117 | * Sélectionne un utilisateur dans une liste Foundry
118 | * @param {import('@playwright/test').Page} page - La page Playwright
119 | * @param {string} username - Le nom de l'utilisateur
```
@@ -0,0 +1,187 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: specs/epic-1-visibility.spec.js >> Epic 1: Core Camera Visibility Control >> FR-1: Hide/Show Participant via Context Menu >> GM can hide a participant from the table via right-click
- Location: specs/epic-1-visibility.spec.js:60:5
# Error details
```
Test timeout of 120000ms exceeded while running "beforeEach" hook.
```
```
Error: page.waitForFunction: Test timeout of 120000ms exceeded.
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- list:
- listitem [ref=e2]:
- text:
- paragraph [ref=e3]: Foundry Virtual Tabletop nécessite une résolution d'écran de 1366px par 768px ou plus. Votre écran a une résolution de 1280px par 720px. Vous devez augmenter votre résolution ou utiliser un autre périphérique d'affichage, sinon certaines fonctionnalités du logiciel ne fonctionneront pas correctement.
- text:
- banner [ref=e5]:
- heading "Donjon & Cie" [level=1] [ref=e6]
- generic [ref=e7]:
- generic [ref=e8]:
- heading "Rejoindre la partie" [level=2] [ref=e9]
- combobox [ref=e14] [cursor=pointer]:
- option [selected]
- option "Gamemaster" [disabled]
- textbox "Mot de passe" [ref=e19]
- contentinfo [ref=e20]:
- button "Rejoindre la partie" [ref=e21] [cursor=pointer]:
- generic: Rejoindre la partie
- generic [ref=e22]:
- heading "Détails de la session" [level=2] [ref=e23]
- generic [ref=e25]: Prochaine partie
- generic [ref=e27]:
- generic [ref=e28]: Joueurs présents
- generic [ref=e30]:
- generic [ref=e31]: "1"
- generic [ref=e32]: /
- generic [ref=e33]: "1"
- generic [ref=e34]:
- heading "Retour à laccueil" [level=2] [ref=e35]
- textbox [ref=e39]
- contentinfo [ref=e40]:
- button "Retour à laccueil" [ref=e41] [cursor=pointer]:
- generic: Retour à laccueil
- article [ref=e42]:
- heading "Description du monde" [level=2] [ref=e43]
- contentinfo [ref=e44]:
- paragraph [ref=e45]: Version 14 Build 361
```
# Test source
```ts
1 | /**
2 | * Helpers pour les tests E2E avec FoundryVTT
3 | *
4 | * Fournit des fonctions utilitaires pour :
5 | * - Attendre que Foundry soit prêt
6 | * - Attendre que le module soit chargé
7 | * - Interagir avec l'UI FoundryVTT
8 | * - Gérer les sélecteurs spécifiques au module
9 | */
10 |
11 | import { expect } from '@playwright/test';
12 |
13 | /**
14 | * Attend que FoundryVTT soit complètement chargé
15 | * @param {import('@playwright/test').Page} page - La page Playwright
16 | * @param {number} timeout - Timeout en ms (défaut: 30000)
17 | */
18 | export async function waitForFoundryReady(page, timeout = 30000) {
> 19 | await page.waitForFunction(() => {
| ^ Error: page.waitForFunction: Test timeout of 120000ms exceeded.
20 | return typeof game !== 'undefined' && game.ready;
21 | }, { timeout });
22 | }
23 |
24 | /**
25 | * Attend que le module Video View Manager soit actif
26 | * @param {import('@playwright/test').Page} page - La page Playwright
27 | * @param {number} timeout - Timeout en ms (défaut: 15000)
28 | */
29 | export async function waitForVVMModule(page, timeout = 15000) {
30 | await page.waitForFunction(() => {
31 | const module = game.modules?.get?.('video-view-manager');
32 | return module?.active === true;
33 | }, { timeout });
34 | }
35 |
36 | /**
37 | * Attend qu'un élément du module soit présent
38 | * @param {import('@playwright/test').Page} page - La page Playwright
39 | * @param {string} selector - Sélecteur CSS
40 | * @param {number} timeout - Timeout en ms (défaut: 10000)
41 | */
42 | export async function waitForVVMElement(page, selector, timeout = 10000) {
43 | await page.waitForSelector(selector, {
44 | state: 'visible',
45 | timeout
46 | });
47 | }
48 |
49 | /**
50 | * Clique sur un bouton dans l'UI Foundry avec retry
51 | * @param {import('@playwright/test').Page} page - La page Playwright
52 | * @param {string|import('@playwright/test').Locator} button - Sélecteur ou Locator
53 | * @param {number} retries - Nombre de tentatives (défaut: 3)
54 | */
55 | export async function clickFoundryButton(page, button, retries = 3) {
56 | for (let i = 0; i < retries; i++) {
57 | try {
58 | const locator = typeof button === 'string' ? page.locator(button) : button;
59 | await locator.click({ timeout: 5000 });
60 | return;
61 | } catch (error) {
62 | if (i === retries - 1) throw error;
63 | await page.waitForTimeout(1000);
64 | }
65 | }
66 | }
67 |
68 | /**
69 | * Ouvre le sidebar de configuration Foundry
70 | * @param {import('@playwright/test').Page} page - La page Playwright
71 | */
72 | export async function openFoundrySidebar(page) {
73 | await clickFoundryButton(page, 'button[aria-label="Configure Settings"]');
74 | await page.waitForSelector('.app-v2.settings', { state: 'visible', timeout: 10000 });
75 | }
76 |
77 | /**
78 | * Ouvre le Director's Board (Epic 2)
79 | * @param {import('@playwright/test').Page} page - La page Playwright
80 | */
81 | export async function openDirectorsBoard(page) {
82 | // Le Director's Board a un bouton dédié dans la sidebar
83 | await page.waitForSelector('button[aria-label*="Director\'s Board"]', { timeout: 10000 });
84 | await clickFoundryButton(page, 'button[aria-label*="Director\'s Board"]');
85 |
86 | // Attendre que le board soit ouvert
87 | await page.waitForSelector('.scrying-pool-directors-board', {
88 | state: 'visible',
89 | timeout: 10000
90 | });
91 | }
92 |
93 | /**
94 | * Ouvre le Player Privacy Panel pour un utilisateur
95 | * @param {import('@playwright/test').Page} page - La page Playwright
96 | * @param {string} userId - L'ID de l'utilisateur
97 | */
98 | export async function openPlayerPrivacyPanel(page, userId) {
99 | // Le panel s'ouvre via les paramètres du module
100 | await openFoundrySidebar(page);
101 |
102 | // Naviguer vers les paramètres du module
103 | await clickFoundryButton(page, 'button:has-text("Video View Manager")');
104 | await page.waitForTimeout(1000);
105 |
106 | // Cliquer sur le bouton Player Privacy
107 | await clickFoundryButton(page, 'button:has-text("Player Privacy")');
108 |
109 | // Attendre le panel
110 | await page.waitForSelector('.sp-player-privacy-panel', {
111 | state: 'visible',
112 | timeout: 10000
113 | });
114 | }
115 |
116 | /**
117 | * Sélectionne un utilisateur dans une liste Foundry
118 | * @param {import('@playwright/test').Page} page - La page Playwright
119 | * @param {string} username - Le nom de l'utilisateur
```
@@ -0,0 +1,187 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: specs/epic-1-visibility.spec.js >> Epic 1: Core Camera Visibility Control >> FR-1: Hide/Show Participant via Context Menu >> GM can hide a participant from the table via right-click
- Location: specs/epic-1-visibility.spec.js:60:5
# Error details
```
Test timeout of 120000ms exceeded while running "beforeEach" hook.
```
```
Error: page.waitForFunction: Test timeout of 120000ms exceeded.
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- list:
- listitem [ref=e2]:
- text:
- paragraph [ref=e3]: Foundry Virtual Tabletop nécessite une résolution d'écran de 1366px par 768px ou plus. Votre écran a une résolution de 1280px par 720px. Vous devez augmenter votre résolution ou utiliser un autre périphérique d'affichage, sinon certaines fonctionnalités du logiciel ne fonctionneront pas correctement.
- text:
- banner [ref=e5]:
- heading "Donjon & Cie" [level=1] [ref=e6]
- generic [ref=e7]:
- generic [ref=e8]:
- heading "Rejoindre la partie" [level=2] [ref=e9]
- combobox [ref=e14] [cursor=pointer]:
- option [selected]
- option "Gamemaster" [disabled]
- textbox "Mot de passe" [ref=e19]
- contentinfo [ref=e20]:
- button "Rejoindre la partie" [ref=e21] [cursor=pointer]:
- generic: Rejoindre la partie
- generic [ref=e22]:
- heading "Détails de la session" [level=2] [ref=e23]
- generic [ref=e25]: Prochaine partie
- generic [ref=e27]:
- generic [ref=e28]: Joueurs présents
- generic [ref=e30]:
- generic [ref=e31]: "1"
- generic [ref=e32]: /
- generic [ref=e33]: "1"
- generic [ref=e34]:
- heading "Retour à laccueil" [level=2] [ref=e35]
- textbox [ref=e39]
- contentinfo [ref=e40]:
- button "Retour à laccueil" [ref=e41] [cursor=pointer]:
- generic: Retour à laccueil
- article [ref=e42]:
- heading "Description du monde" [level=2] [ref=e43]
- contentinfo [ref=e44]:
- paragraph [ref=e45]: Version 14 Build 361
```
# Test source
```ts
1 | /**
2 | * Helpers pour les tests E2E avec FoundryVTT
3 | *
4 | * Fournit des fonctions utilitaires pour :
5 | * - Attendre que Foundry soit prêt
6 | * - Attendre que le module soit chargé
7 | * - Interagir avec l'UI FoundryVTT
8 | * - Gérer les sélecteurs spécifiques au module
9 | */
10 |
11 | import { expect } from '@playwright/test';
12 |
13 | /**
14 | * Attend que FoundryVTT soit complètement chargé
15 | * @param {import('@playwright/test').Page} page - La page Playwright
16 | * @param {number} timeout - Timeout en ms (défaut: 30000)
17 | */
18 | export async function waitForFoundryReady(page, timeout = 30000) {
> 19 | await page.waitForFunction(() => {
| ^ Error: page.waitForFunction: Test timeout of 120000ms exceeded.
20 | return typeof game !== 'undefined' && game.ready;
21 | }, { timeout });
22 | }
23 |
24 | /**
25 | * Attend que le module Video View Manager soit actif
26 | * @param {import('@playwright/test').Page} page - La page Playwright
27 | * @param {number} timeout - Timeout en ms (défaut: 15000)
28 | */
29 | export async function waitForVVMModule(page, timeout = 15000) {
30 | await page.waitForFunction(() => {
31 | const module = game.modules?.get?.('video-view-manager');
32 | return module?.active === true;
33 | }, { timeout });
34 | }
35 |
36 | /**
37 | * Attend qu'un élément du module soit présent
38 | * @param {import('@playwright/test').Page} page - La page Playwright
39 | * @param {string} selector - Sélecteur CSS
40 | * @param {number} timeout - Timeout en ms (défaut: 10000)
41 | */
42 | export async function waitForVVMElement(page, selector, timeout = 10000) {
43 | await page.waitForSelector(selector, {
44 | state: 'visible',
45 | timeout
46 | });
47 | }
48 |
49 | /**
50 | * Clique sur un bouton dans l'UI Foundry avec retry
51 | * @param {import('@playwright/test').Page} page - La page Playwright
52 | * @param {string|import('@playwright/test').Locator} button - Sélecteur ou Locator
53 | * @param {number} retries - Nombre de tentatives (défaut: 3)
54 | */
55 | export async function clickFoundryButton(page, button, retries = 3) {
56 | for (let i = 0; i < retries; i++) {
57 | try {
58 | const locator = typeof button === 'string' ? page.locator(button) : button;
59 | await locator.click({ timeout: 5000 });
60 | return;
61 | } catch (error) {
62 | if (i === retries - 1) throw error;
63 | await page.waitForTimeout(1000);
64 | }
65 | }
66 | }
67 |
68 | /**
69 | * Ouvre le sidebar de configuration Foundry
70 | * @param {import('@playwright/test').Page} page - La page Playwright
71 | */
72 | export async function openFoundrySidebar(page) {
73 | await clickFoundryButton(page, 'button[aria-label="Configure Settings"]');
74 | await page.waitForSelector('.app-v2.settings', { state: 'visible', timeout: 10000 });
75 | }
76 |
77 | /**
78 | * Ouvre le Director's Board (Epic 2)
79 | * @param {import('@playwright/test').Page} page - La page Playwright
80 | */
81 | export async function openDirectorsBoard(page) {
82 | // Le Director's Board a un bouton dédié dans la sidebar
83 | await page.waitForSelector('button[aria-label*="Director\'s Board"]', { timeout: 10000 });
84 | await clickFoundryButton(page, 'button[aria-label*="Director\'s Board"]');
85 |
86 | // Attendre que le board soit ouvert
87 | await page.waitForSelector('.scrying-pool-directors-board', {
88 | state: 'visible',
89 | timeout: 10000
90 | });
91 | }
92 |
93 | /**
94 | * Ouvre le Player Privacy Panel pour un utilisateur
95 | * @param {import('@playwright/test').Page} page - La page Playwright
96 | * @param {string} userId - L'ID de l'utilisateur
97 | */
98 | export async function openPlayerPrivacyPanel(page, userId) {
99 | // Le panel s'ouvre via les paramètres du module
100 | await openFoundrySidebar(page);
101 |
102 | // Naviguer vers les paramètres du module
103 | await clickFoundryButton(page, 'button:has-text("Video View Manager")');
104 | await page.waitForTimeout(1000);
105 |
106 | // Cliquer sur le bouton Player Privacy
107 | await clickFoundryButton(page, 'button:has-text("Player Privacy")');
108 |
109 | // Attendre le panel
110 | await page.waitForSelector('.sp-player-privacy-panel', {
111 | state: 'visible',
112 | timeout: 10000
113 | });
114 | }
115 |
116 | /**
117 | * Sélectionne un utilisateur dans une liste Foundry
118 | * @param {import('@playwright/test').Page} page - La page Playwright
119 | * @param {string} username - Le nom de l'utilisateur
```