diff --git a/invokeai/frontend/web/src/common/hooks/useMiddleClickOpenInNewTab.ts b/invokeai/frontend/web/src/common/hooks/useMiddleClickOpenInNewTab.ts index 92f0c58aeaf..a165b0064ba 100644 --- a/invokeai/frontend/web/src/common/hooks/useMiddleClickOpenInNewTab.ts +++ b/invokeai/frontend/web/src/common/hooks/useMiddleClickOpenInNewTab.ts @@ -1,5 +1,5 @@ import { useAppSelector } from 'app/store/storeHooks'; -import { openMediaInNewTab } from 'features/auth/hooks/useMediaCookieRefresh'; +import { openMediaInNewTab } from 'common/util/openMediaInNewTab'; import { selectSystemShouldUseMiddleClickToOpenInNewTab } from 'features/system/store/systemSlice'; import type { RefObject } from 'react'; import { useEffect } from 'react'; diff --git a/invokeai/frontend/web/src/common/util/openImageInNewTab.ts b/invokeai/frontend/web/src/common/util/openImageInNewTab.ts deleted file mode 100644 index 3e8e13334c2..00000000000 --- a/invokeai/frontend/web/src/common/util/openImageInNewTab.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const openImageInNewTab = (imageUrl: string) => { - window.open(imageUrl, '_blank', 'noopener,noreferrer'); -}; diff --git a/invokeai/frontend/web/src/common/util/openMediaInNewTab.test.ts b/invokeai/frontend/web/src/common/util/openMediaInNewTab.test.ts new file mode 100644 index 00000000000..2fc8610fe0a --- /dev/null +++ b/invokeai/frontend/web/src/common/util/openMediaInNewTab.test.ts @@ -0,0 +1,37 @@ +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; + +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { openMediaInNewTab } from './openMediaInNewTab'; + +const readSource = (relativePath: string) => + readFileSync(fileURLToPath(new URL(relativePath, import.meta.url)), 'utf8'); + +describe('openMediaInNewTab', () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('opens the media URL directly in a new tab', () => { + const open = vi.fn(() => null); + vi.stubGlobal('window', { open }); + + openMediaInNewTab('api/v1/images/i/test.png/full'); + + expect(open).toHaveBeenCalledWith('api/v1/images/i/test.png/full', '_blank', 'noopener,noreferrer'); + }); + + it('does not use an about:blank intermediary', () => { + const source = readSource('./openMediaInNewTab.ts'); + + expect(source).not.toContain('about:blank'); + }); + + it('keeps middle-click opening on the shared direct opener', () => { + const source = readSource('../hooks/useMiddleClickOpenInNewTab.ts'); + + expect(source).toContain("from 'common/util/openMediaInNewTab'"); + expect(source).not.toContain('useMediaCookieRefresh'); + }); +}); diff --git a/invokeai/frontend/web/src/common/util/openMediaInNewTab.ts b/invokeai/frontend/web/src/common/util/openMediaInNewTab.ts new file mode 100644 index 00000000000..b9dcec1df56 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/openMediaInNewTab.ts @@ -0,0 +1,3 @@ +export const openMediaInNewTab = (mediaUrl: string) => { + window.open(mediaUrl, '_blank', 'noopener,noreferrer'); +}; diff --git a/invokeai/frontend/web/src/features/auth/hooks/useMediaCookieRefresh.ts b/invokeai/frontend/web/src/features/auth/hooks/useMediaCookieRefresh.ts index 46eef8e012e..885c3bdae51 100644 --- a/invokeai/frontend/web/src/features/auth/hooks/useMediaCookieRefresh.ts +++ b/invokeai/frontend/web/src/features/auth/hooks/useMediaCookieRefresh.ts @@ -37,16 +37,6 @@ export const waitForMediaCookieSelfHeal = (): Promise => { }); }; -export const openMediaInNewTab = (url: string) => { - const tab = window.open('about:blank', '_blank'); - if (tab) { - tab.opener = null; - } - void waitForMediaCookieSelfHeal().then(() => { - tab?.location.replace(url); - }); -}; - export const abortAndWaitForPendingRefreshes = async (pending: Set) => { for (const refresh of pending) { refresh.abort(); diff --git a/invokeai/frontend/web/src/features/auth/store/protectedMediaConsumers.test.ts b/invokeai/frontend/web/src/features/auth/store/protectedMediaConsumers.test.ts index 370c618fe65..22829c3b2db 100644 --- a/invokeai/frontend/web/src/features/auth/store/protectedMediaConsumers.test.ts +++ b/invokeai/frontend/web/src/features/auth/store/protectedMediaConsumers.test.ts @@ -31,10 +31,11 @@ describe('protected media consumers', () => { expect(source).not.toContain(rawUsage); }); - it('waits for media-cookie self-heal before opening the video metadata link', () => { + it('opens the video metadata link through the shared direct opener', () => { const source = readConsumer('../../gallery/components/ImageMetadataViewer/VideoMetadataViewer.tsx'); expect(source).toContain('openMediaInNewTab'); + expect(source).toContain("from 'common/util/openMediaInNewTab'"); expect(source).not.toContain('href={video.video_url}'); expect(source).not.toContain('href={videoUrl}'); }); diff --git a/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemOpenInNewTab.tsx b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemOpenInNewTab.tsx index 4d086136d92..0494218a5f7 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemOpenInNewTab.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemOpenInNewTab.tsx @@ -1,5 +1,5 @@ import { IconMenuItem } from 'common/components/IconMenuItem'; -import { openImageInNewTab } from 'common/util/openImageInNewTab'; +import { openMediaInNewTab } from 'common/util/openMediaInNewTab'; import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,7 +9,7 @@ export const ContextMenuItemOpenInNewTab = memo(() => { const { t } = useTranslation(); const imageDTO = useImageDTOContext(); const onClick = useCallback(() => { - openImageInNewTab(imageDTO.image_url); + openMediaInNewTab(imageDTO.image_url); }, [imageDTO]); return ( diff --git a/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemOpenInNewTabVideo.tsx b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemOpenInNewTabVideo.tsx index d318c1cb09c..a259b21935e 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemOpenInNewTabVideo.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemOpenInNewTabVideo.tsx @@ -1,5 +1,5 @@ import { MenuItem } from '@invoke-ai/ui-library'; -import { openMediaInNewTab } from 'features/auth/hooks/useMediaCookieRefresh'; +import { openMediaInNewTab } from 'common/util/openMediaInNewTab'; import { useVideoDTOContext } from 'features/gallery/contexts/VideoDTOContext'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/VideoMetadataViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/VideoMetadataViewer.tsx index fc4592fe9a0..f676b992957 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/VideoMetadataViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/VideoMetadataViewer.tsx @@ -1,6 +1,6 @@ import { ExternalLink, Flex, Tab, TabList, TabPanel, TabPanels, Tabs, Text } from '@invoke-ai/ui-library'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; -import { openMediaInNewTab } from 'features/auth/hooks/useMediaCookieRefresh'; +import { openMediaInNewTab } from 'common/util/openMediaInNewTab'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { useGetVideoMetadataQuery, useGetVideoWorkflowQuery } from 'services/api/endpoints/videos'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentVideoPreview.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentVideoPreview.tsx index 92dc9c897d3..966903a57e5 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentVideoPreview.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentVideoPreview.tsx @@ -5,7 +5,8 @@ import { useStore } from '@nanostores/react'; import { useAppSelector, useAppStore } from 'app/store/storeHooks'; import { useClipboard } from 'common/hooks/useClipboard'; import { useDownloadItem } from 'common/hooks/useDownloadImage'; -import { isMediaCookieSelfHealPending, openMediaInNewTab } from 'features/auth/hooks/useMediaCookieRefresh'; +import { openMediaInNewTab } from 'common/util/openMediaInNewTab'; +import { isMediaCookieSelfHealPending } from 'features/auth/hooks/useMediaCookieRefresh'; import { useMediaUrl } from 'features/auth/store/mediaCookieRefresh'; import { useDeleteVideoModalApi } from 'features/deleteVideoModal/store/state'; import { multipleVideoDndSource, singleVideoDndSource } from 'features/dnd/dnd'; diff --git a/invokeai/frontend/web/src/features/gallery/videoReviewRegressions.test.ts b/invokeai/frontend/web/src/features/gallery/videoReviewRegressions.test.ts index 1432820afc4..5f3bf632b25 100644 --- a/invokeai/frontend/web/src/features/gallery/videoReviewRegressions.test.ts +++ b/invokeai/frontend/web/src/features/gallery/videoReviewRegressions.test.ts @@ -59,12 +59,14 @@ describe('video review regressions', () => { expect(locale).toContain('"problemDownloadingMedia": "Unable to Download Media"'); }); - it('waits for cookie self-heal before opening protected video URLs', () => { + it('opens protected video URLs through the shared direct opener', () => { const preview = readSource('./components/ImageViewer/CurrentVideoPreview.tsx'); const contextMenu = readSource('./components/ContextMenu/MenuItems/ContextMenuItemOpenInNewTabVideo.tsx'); expect(preview).toContain('openMediaInNewTab'); expect(contextMenu).toContain('openMediaInNewTab'); + expect(preview).toContain("from 'common/util/openMediaInNewTab'"); + expect(contextMenu).toContain("from 'common/util/openMediaInNewTab'"); expect(preview).not.toContain('window.open(videoDTO.video_url'); expect(contextMenu).not.toContain('window.open(videoDTO.video_url'); });