Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export default defineConfig({
timeout: 15 * 1000,
},
fullyParallel: false,
retries: process.env.CI ? 1 : 0,
workers: 1,
reporter: [['list'], ['html', { open: 'never' }]],
webServer: {
Expand All @@ -31,8 +32,8 @@ export default defineConfig({
...devices['Desktop Chrome'],
baseURL: `http://127.0.0.1:${mockFrontendPort}`,
channel: process.env.PLAYWRIGHT_CHANNEL || undefined,
trace: 'retain-on-failure',
trace: process.env.CI ? 'retain-on-first-failure' : 'retain-on-failure',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
video: process.env.CI ? 'retain-on-first-failure' : 'retain-on-failure',
},
});
38 changes: 33 additions & 5 deletions src/components/WorkflowTabsBar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useLayoutEffect, useRef } from 'react';
import { useCallback, useEffect, useLayoutEffect, useRef } from 'react';
import { Plus, X } from 'lucide-react';
import { useShallow } from 'zustand/react/shallow';
import { useFlowStore } from '../stores/useFlowStore';
Expand All @@ -10,6 +10,7 @@ import { markWorkflowTabClosed } from '../studio/useWorkflowBackendSync';

export default function WorkflowTabsBar() {
const tabListRef = useRef<HTMLDivElement>(null);
const pendingTabRevealRef = useRef<number | null>(null);
const pendingSnapshotSaveRef = useRef(false);
const {
workflowTabs,
Expand Down Expand Up @@ -37,6 +38,34 @@ export default function WorkflowTabsBar() {
const graphBinding = useStudioStore((state) => state.graphBinding);
const workflowCanvasHydrated = useStudioStore((state) => state.workflowCanvasHydrated);

const revealTabItem = useCallback((tabItem: HTMLElement) => {
const tabList = tabListRef.current;
if (!tabList) return;

if (pendingTabRevealRef.current !== null) {
window.cancelAnimationFrame(pendingTabRevealRef.current);
}

revealHorizontalItem(tabList, tabItem);
pendingTabRevealRef.current = window.requestAnimationFrame(() => {
pendingTabRevealRef.current = null;
if (!tabItem.isConnected || !tabList.contains(tabItem)) return;
// Native focus scrolling can run after the focus event and reveal only
// the tab button. Recheck after layout so the adjacent close action is
// kept inside the horizontal scrollport as well.
revealHorizontalItem(tabList, tabItem);
});
}, []);

useEffect(
() => () => {
if (pendingTabRevealRef.current !== null) {
window.cancelAnimationFrame(pendingTabRevealRef.current);
}
},
[],
);

useEffect(() => {
if (!workflowCanvasHydrated) return;
ensureWorkflowTabs();
Expand Down Expand Up @@ -66,8 +95,8 @@ export default function WorkflowTabsBar() {
useLayoutEffect(() => {
const tabList = tabListRef.current;
const activeTab = tabList?.querySelector<HTMLElement>('[aria-selected="true"]')?.parentElement;
if (tabList && activeTab) revealHorizontalItem(tabList, activeTab);
}, [activeWorkflowTabId, workflowTabs.length]);
if (activeTab) revealTabItem(activeTab);
}, [activeWorkflowTabId, revealTabItem, workflowTabs.length]);

if (workflowTabs.length === 0 || !activeWorkflowTabId) return null;

Expand Down Expand Up @@ -108,9 +137,8 @@ export default function WorkflowTabsBar() {
data-testid={`workflow-tab-${tab.id}`}
onSelect={() => switchWorkflowTab(tab.id)}
onFocus={(event) => {
const tabList = event.currentTarget.closest<HTMLElement>('[data-testid="workflow-tabs-scroll"]');
const tabItem = event.currentTarget.parentElement;
if (tabList && tabItem) revealHorizontalItem(tabList, tabItem);
if (tabItem) revealTabItem(tabItem);
}}
className="min-w-0 flex-1 justify-start rounded-none px-2 text-left text-xs leading-none focus-visible:outline-inset"
>
Expand Down
53 changes: 31 additions & 22 deletions tests/e2e/studio-mocked/studio-mocked.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2461,10 +2461,12 @@ test('mocked Studio keeps model health contextual while exposing every authored
await expect(page.getByTestId('template-browser-use-flux_fill_inpaint')).toBeVisible();
await page.getByTestId('template-browser-search').fill('canny');
await expect(page.getByTestId('template-browser-use-flux_control_canny')).toBeVisible();
await page.getByTestId('template-browser-model-filter').click();
const modelFilter = page.getByTestId('template-browser-model-filter');
await modelFilter.click();
await expect(page.getByRole('option', { name: 'FLUX.1-dev', exact: true })).toBeVisible();
await expect(page.getByRole('option', { name: 'FLUX.1-Krea-dev', exact: true })).toBeVisible();
await page.keyboard.press('Escape');
await modelFilter.click();
await expect(page.getByRole('listbox')).toBeHidden();

await page.getByTestId('template-browser-search').fill('flux schnell');
await expect(page.getByTestId('template-usage-warning-flux_schnell_text_to_image')).toHaveCount(0);
Expand Down Expand Up @@ -6427,26 +6429,31 @@ test('previous video and audio renders open in-app without replacing the current

const videoStage = page.locator(`.react-flow__node[data-id="${videoState.exporterId}"]`);
const emptyVideo = page.getByTestId(`node-preview-empty-${videoState.exporterId}-preview`);
await expect(videoStage).toBeVisible();
await expect(emptyVideo).toBeVisible();
await expect(videoStage.getByText('Previous')).toBeVisible();
await videoStage.getByTitle(/frames from|Video from/).click();
await expect(page.getByTestId('media-viewer-dialog')).toBeVisible();
await expect(page.getByTestId('media-viewer-video')).toBeVisible();
const mediaViewer = page.getByTestId('media-viewer-dialog');
await expect(mediaViewer).toBeVisible();
await expect(mediaViewer.getByTestId('media-viewer-video')).toBeVisible();
await expect(emptyVideo).toBeVisible();
await page.getByRole('button', { name: 'Close media viewer' }).click();
await expect(page.getByTestId('media-viewer-dialog')).toHaveCount(0);
await mediaViewer.getByRole('button', { name: 'Close media viewer' }).click();
await expect(mediaViewer).toHaveCount(0);
await videoStage.getByTitle(/frames from|Video from/).click();
await expect(page.getByTestId('media-viewer-dialog')).toBeVisible();
await page.getByRole('button', { name: 'Download video' }).click();
await expect(page.getByTestId('media-export-dialog')).toBeVisible();
await page.getByLabel('Download format').click();
await expect(mediaViewer).toBeVisible();
await mediaViewer.getByRole('button', { name: 'Download video' }).click();
const mediaExport = page.getByTestId('media-export-dialog');
await expect(mediaExport).toBeVisible();
await mediaExport.getByLabel('Download format').click();
await expect(page.getByRole('option', { name: 'MP4 (H.264 + AAC)' })).toBeVisible();
await expect(page.getByRole('option', { name: 'WebM (VP9 + Opus)' })).toBeVisible();
await expect(page.getByRole('option', { name: 'MOV (ProRes + PCM)' })).toBeVisible();
await expect(page.getByRole('option', { name: 'Animated GIF' })).toBeVisible();
await page.keyboard.press('Escape');
await page.getByRole('button', { name: 'Cancel' }).click();
await page.getByRole('button', { name: 'Close media viewer' }).click();
await mediaExport.getByRole('button', { name: 'Cancel' }).click();
await expect(mediaExport).toHaveCount(0);
await mediaViewer.getByRole('button', { name: 'Close media viewer' }).click();
await expect(mediaViewer).toHaveCount(0);

await videoStage.getByRole('button', { name: 'Output actions' }).click();
const copyUrlAction = page.getByRole('menuitem', { name: 'Copy URL' });
Expand All @@ -6457,12 +6464,12 @@ test('previous video and audio renders open in-app without replacing the current
return url.pathname === '/cache/history/video.mp4' && url.searchParams.get('download_format') === 'webm';
});
await page.getByRole('menuitem', { name: /^Download/ }).click();
await expect(page.getByTestId('media-export-dialog')).toBeVisible();
await page.getByLabel('Download format').click();
await expect(mediaExport).toBeVisible();
await mediaExport.getByLabel('Download format').click();
await page.getByRole('option', { name: 'WebM (VP9 + Opus)' }).click();
await page.getByRole('button', { name: 'Download', exact: true }).click();
await mediaExport.getByRole('button', { name: 'Download', exact: true }).click();
await convertedDownload;
await expect(page.getByTestId('media-export-dialog')).toHaveCount(0);
await expect(mediaExport).toHaveCount(0);
await expect(copyUrlAction).toHaveCount(0);

await page.evaluate(({ exporterId }) => {
Expand All @@ -6488,10 +6495,10 @@ test('previous video and audio renders open in-app without replacing the current
await expect(videoActions).toHaveClass(/rounded-full/);
await videoActions.click();
await page.getByRole('menuitem', { name: /^Download/ }).click();
await expect(page.getByTestId('media-export-dialog')).toBeVisible();
await expect(page.getByLabel('Download format')).toBeVisible();
await page.getByRole('button', { name: 'Cancel' }).click();
await expect(page.getByTestId('media-export-dialog')).toHaveCount(0);
await expect(mediaExport).toBeVisible();
await expect(mediaExport.getByLabel('Download format')).toBeVisible();
await mediaExport.getByRole('button', { name: 'Cancel' }).click();
await expect(mediaExport).toHaveCount(0);

const audioState = await page.evaluate(async () => {
await window.__MODIFF_E2E__!.applyTemplate('ace_step_text_to_audio');
Expand All @@ -6514,10 +6521,12 @@ test('previous video and audio renders open in-app without replacing the current

const audioStage = page.locator(`.react-flow__node[data-id="${audioState.exporterId}"]`);
const emptyAudio = page.getByTestId(`node-preview-empty-${audioState.exporterId}-preview`);
await expect(videoStage).toHaveCount(0);
await expect(audioStage).toBeVisible();
await expect(emptyAudio).toBeVisible();
await audioStage.getByTitle(/Audio from/).click();
await expect(page.getByTestId('media-viewer-dialog')).toBeVisible();
await expect(page.getByTestId('media-viewer-audio')).toBeVisible();
await expect(mediaViewer).toBeVisible();
await expect(mediaViewer.getByTestId('media-viewer-audio')).toBeVisible();
await expect(emptyAudio).toBeVisible();
});

Expand Down
Loading