diff --git a/browser_tests/fixtures/ComfyPage.ts b/browser_tests/fixtures/ComfyPage.ts index c32dd3937..26104e617 100644 --- a/browser_tests/fixtures/ComfyPage.ts +++ b/browser_tests/fixtures/ComfyPage.ts @@ -11,6 +11,7 @@ import type { useWorkspaceStore } from '../../src/stores/workspaceStore' import { NodeBadgeMode } from '../../src/types/nodeSource' import { ComfyActionbar } from '../helpers/actionbar' import { ComfyTemplates } from '../helpers/templates' +import { LocationMock } from '../helpers/locationMock' import { ComfyMouse } from './ComfyMouse' import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox' import { SettingDialog } from './components/SettingDialog' @@ -144,6 +145,7 @@ export class ComfyPage { public readonly templates: ComfyTemplates public readonly settingDialog: SettingDialog public readonly confirmDialog: ConfirmDialog + public readonly locationMock: LocationMock /** Worker index to test user ID */ public readonly userIds: string[] = [] @@ -172,6 +174,7 @@ export class ComfyPage { this.templates = new ComfyTemplates(page) this.settingDialog = new SettingDialog(page, this) this.confirmDialog = new ConfirmDialog(page) + this.locationMock = new LocationMock(page) } convertLeafToContent(structure: FolderStructure): FolderStructure { @@ -272,11 +275,19 @@ export class ComfyPage { async setup({ clearStorage = true, - mockReleases = true + mockReleases = true, + mockLocation = false }: { clearStorage?: boolean mockReleases?: boolean + mockLocation?: boolean | Parameters[0] } = {}) { + // Setup location mock if requested + if (mockLocation) { + const config = typeof mockLocation === 'boolean' ? undefined : mockLocation + await this.locationMock.setupLocationMock(config) + } + await this.goto() // Mock release endpoint to prevent changelog popups diff --git a/browser_tests/helpers/locationMock.ts b/browser_tests/helpers/locationMock.ts new file mode 100644 index 000000000..d04d48540 --- /dev/null +++ b/browser_tests/helpers/locationMock.ts @@ -0,0 +1,142 @@ +import type { Page } from '@playwright/test' + +/** + * Mock location object for testing navigation and URL manipulation + */ +export class LocationMock { + constructor(private page: Page) {} + + /** + * Mock the location object in the page context + * @param mockConfig Configuration for the mock location + */ + async setupLocationMock(mockConfig?: { + href?: string + origin?: string + pathname?: string + search?: string + hash?: string + hostname?: string + port?: string + protocol?: string + }) { + await this.page.addInitScript((config) => { + const defaultUrl = config?.href || window.location.href + const url = new URL(defaultUrl) + + // Create a mock location object + const mockLocation = { + href: config?.href || url.href, + origin: config?.origin || url.origin, + protocol: config?.protocol || url.protocol, + host: url.host, + hostname: config?.hostname || url.hostname, + port: config?.port || url.port, + pathname: config?.pathname || url.pathname, + search: config?.search || url.search, + hash: config?.hash || url.hash, + assign: (newUrl: string) => { + console.log(`[Mock] location.assign called with: ${newUrl}`) + mockLocation.href = newUrl + // Trigger navigation event if needed + window.dispatchEvent(new Event('popstate')) + }, + replace: (newUrl: string) => { + console.log(`[Mock] location.replace called with: ${newUrl}`) + mockLocation.href = newUrl + // Trigger navigation event if needed + window.dispatchEvent(new Event('popstate')) + }, + reload: () => { + console.log('[Mock] location.reload called') + // Trigger reload event if needed + window.dispatchEvent(new Event('beforeunload')) + }, + toString: () => mockLocation.href + } + + // Override window.location + Object.defineProperty(window, 'location', { + value: mockLocation, + writable: true, + configurable: true + }) + + // Also override document.location + Object.defineProperty(document, 'location', { + value: mockLocation, + writable: true, + configurable: true + }) + }, mockConfig) + } + + /** + * Update the mock location during test execution + */ + async updateLocation(updates: Partial<{ + href: string + pathname: string + search: string + hash: string + }>) { + await this.page.evaluate((updates) => { + const location = window.location as any + Object.keys(updates).forEach((key) => { + if (location[key] !== undefined) { + location[key] = updates[key as keyof typeof updates] + } + }) + }, updates) + } + + /** + * Get the current mock location values + */ + async getLocation() { + return await this.page.evaluate(() => { + const loc = window.location + return { + href: loc.href, + origin: loc.origin, + protocol: loc.protocol, + host: loc.host, + hostname: loc.hostname, + port: loc.port, + pathname: loc.pathname, + search: loc.search, + hash: loc.hash + } + }) + } + + /** + * Simulate navigation to a new URL + */ + async navigateTo(url: string) { + await this.page.evaluate((url) => { + const location = window.location as any + location.assign(url) + }, url) + } + + /** + * Simulate location.replace + */ + async replaceTo(url: string) { + await this.page.evaluate((url) => { + const location = window.location as any + location.replace(url) + }, url) + } + + /** + * Simulate location.reload + */ + async reload() { + await this.page.evaluate(() => { + const location = window.location as any + location.reload() + }) + } +} \ No newline at end of file diff --git a/browser_tests/locationMock.example.test.ts b/browser_tests/locationMock.example.test.ts new file mode 100644 index 000000000..5e73169a3 --- /dev/null +++ b/browser_tests/locationMock.example.test.ts @@ -0,0 +1,96 @@ +import { expect } from '@playwright/test' +import { comfyPageFixture as test } from './fixtures/ComfyPage' +import { LocationMock } from './helpers/locationMock' + +test.describe('Location Mock Example', () => { + test('should mock location object', async ({ page, comfyPage }) => { + const locationMock = new LocationMock(page) + + // Setup location mock before navigating to the page + await locationMock.setupLocationMock({ + href: 'http://example.com/test', + pathname: '/test', + search: '?query=value', + hash: '#section' + }) + + // Navigate to your app + await comfyPage.goto() + + // Verify the mock is working + const location = await locationMock.getLocation() + expect(location.pathname).toBe('/test') + expect(location.search).toBe('?query=value') + expect(location.hash).toBe('#section') + + // Test navigation + await locationMock.navigateTo('http://example.com/new-page') + const newLocation = await locationMock.getLocation() + expect(newLocation.href).toBe('http://example.com/new-page') + + // Test updating specific properties + await locationMock.updateLocation({ + pathname: '/updated-path', + search: '?new=param' + }) + + const updatedLocation = await locationMock.getLocation() + expect(updatedLocation.pathname).toBe('/updated-path') + expect(updatedLocation.search).toBe('?new=param') + }) + + test('should handle location methods', async ({ page, comfyPage }) => { + const locationMock = new LocationMock(page) + + await locationMock.setupLocationMock({ + href: 'http://localhost:5173/' + }) + + await comfyPage.goto() + + // Test location.assign + await page.evaluate(() => { + window.location.assign('/new-route') + }) + + // Check console for mock output + const consoleMessages: string[] = [] + page.on('console', (msg) => { + if (msg.text().includes('[Mock]')) { + consoleMessages.push(msg.text()) + } + }) + + await locationMock.navigateTo('/another-route') + await locationMock.replaceTo('/replaced-route') + await locationMock.reload() + + // Verify mock methods were called + expect(consoleMessages.some((msg) => msg.includes('location.assign'))).toBeTruthy() + expect(consoleMessages.some((msg) => msg.includes('location.replace'))).toBeTruthy() + expect(consoleMessages.some((msg) => msg.includes('location.reload'))).toBeTruthy() + }) + + test('should work with Happy DOM globals', async ({ page, comfyPage }) => { + // Set environment variable for Happy DOM URL + process.env.HAPPY_DOM_URL = 'http://custom-domain.com/' + + const locationMock = new LocationMock(page) + await locationMock.setupLocationMock() + + await comfyPage.goto() + + // Verify location is mocked correctly + const location = await page.evaluate(() => ({ + href: window.location.href, + origin: window.location.origin, + canAssign: typeof window.location.assign === 'function', + canReplace: typeof window.location.replace === 'function', + canReload: typeof window.location.reload === 'function' + })) + + expect(location.canAssign).toBeTruthy() + expect(location.canReplace).toBeTruthy() + expect(location.canReload).toBeTruthy() + }) +}) \ No newline at end of file diff --git a/scripts/setup-browser-globals.js b/scripts/setup-browser-globals.js index 4224bd36e..faf683f73 100644 --- a/scripts/setup-browser-globals.js +++ b/scripts/setup-browser-globals.js @@ -6,17 +6,43 @@ if (typeof globalThis.__USE_PROD_CONFIG__ === 'undefined') { globalThis.__USE_PROD_CONFIG__ = false; } -// Create a happy-dom window instance +// Create a happy-dom window instance with configurable URL +const defaultUrl = (typeof globalThis.process !== 'undefined' && globalThis.process.env?.HAPPY_DOM_URL) || 'http://localhost:5173/'; const window = new Window({ - url: 'http://localhost:5173/', + url: defaultUrl, width: 1024, height: 768 }); +// Mock location with additional properties for testing +const mockLocation = { + ...window.location, + href: defaultUrl, + origin: new URL(defaultUrl).origin, + protocol: new URL(defaultUrl).protocol, + host: new URL(defaultUrl).host, + hostname: new URL(defaultUrl).hostname, + port: new URL(defaultUrl).port, + pathname: new URL(defaultUrl).pathname, + search: new URL(defaultUrl).search, + hash: new URL(defaultUrl).hash, + assign: (url) => { + console.log(`[Mock] location.assign called with: ${url}`); + mockLocation.href = url; + }, + replace: (url) => { + console.log(`[Mock] location.replace called with: ${url}`); + mockLocation.href = url; + }, + reload: () => { + console.log('[Mock] location.reload called'); + } +}; + // Expose DOM globals (only set if not already defined) if (!globalThis.window) globalThis.window = window; if (!globalThis.document) globalThis.document = window.document; -if (!globalThis.location) globalThis.location = window.location; +if (!globalThis.location) globalThis.location = mockLocation; if (!globalThis.navigator) { try { globalThis.navigator = window.navigator;