diff --git a/apps/docs/components/workflow-preview/workflow-preview.tsx b/apps/docs/components/workflow-preview/workflow-preview.tsx index 15535e6a66d..150f43a42bd 100644 --- a/apps/docs/components/workflow-preview/workflow-preview.tsx +++ b/apps/docs/components/workflow-preview/workflow-preview.tsx @@ -136,6 +136,7 @@ function PreviewFlow({ nodes={nodes} edges={initialEdges} onNodesChange={onNodesChange} + onNodeClick={onNodeClick ? (_, node) => onNodeClick(node.id) : undefined} onPaneClick={onPaneClick} nodeTypes={NODE_TYPES} edgeTypes={EDGE_TYPES} diff --git a/apps/docs/scripts/benchmark-readiness.ts b/apps/docs/scripts/benchmark-readiness.ts index c58764704ae..348bcb4a396 100644 --- a/apps/docs/scripts/benchmark-readiness.ts +++ b/apps/docs/scripts/benchmark-readiness.ts @@ -949,7 +949,7 @@ async function interactions(browser: Browser, profile: Profile, repetition: numb } }) await apiControls(run, profile, repetition) - await measure(profile, '/workflows', repetition, 'workflow-inspector-control', async () => { + await measure(profile, '/workflows', repetition, 'workflow-canvas-control', async () => { await run.page.goto(new URL('/workflows', base).href, { waitUntil: 'domcontentloaded' }) await meaningful(run.page, new URL('/workflows', base).pathname) const trigger = run.page @@ -963,20 +963,65 @@ async function interactions(browser: Browser, profile: Profile, repetition: numb await dialog.waitFor() await painted(run.page) const feedbackMs = performance.now() - start - const block = dialog - .getByRole('group', { name: 'Inspect a block' }) - .getByRole('button') - .first() - if (await block.count()) { - await block.click() - await painted(run.page) - } + const canvas = dialog.locator('.react-flow__pane') + const viewport = dialog.locator('.react-flow__viewport') + const node = dialog.locator('.react-flow__node').first() + await expect(node).toBeVisible() + await painted(run.page) + const bounds = await canvas.boundingBox() + if (!bounds) throw new Error('Workflow canvas has no visible bounds') + const transform = (element: Locator) => + element.evaluate((element) => { + const matrix = new DOMMatrixReadOnly(getComputedStyle(element).transform) + return { x: matrix.e, y: matrix.f, scale: matrix.a } + }) + const beforePan = await transform(viewport) + const x = bounds.x + bounds.width / 2 + const y = bounds.y + bounds.height - 24 + await run.page.mouse.move(x, y) + await run.page.mouse.down() + await run.page.mouse.move(x + 24, y - 24, { steps: 5 }) + await run.page.mouse.up() + await expect.poll(async () => (await transform(viewport)).x).not.toBe(beforePan.x) + const afterPan = await transform(viewport) + expect(afterPan.scale).toBe(beforePan.scale) + const beforeZoom = afterPan.scale + await run.page.mouse.wheel(0, -80) + await expect + .poll(() => + viewport.evaluate( + (element) => new DOMMatrixReadOnly(getComputedStyle(element).transform).a + ) + ) + .toBeGreaterThan(beforeZoom) + await node.click({ trial: true, position: { x: 8, y: 8 } }) + const afterZoom = await transform(viewport) + const nodeBefore = await transform(node) + const nodeBounds = await node.boundingBox() + if (!nodeBounds) throw new Error('Workflow node has no visible bounds') + await run.page.mouse.move(nodeBounds.x + nodeBounds.width / 2, nodeBounds.y + 8) + await run.page.mouse.down() + await run.page.mouse.move(nodeBounds.x + nodeBounds.width / 2 + 20, nodeBounds.y + 28, { + steps: 5, + }) + await run.page.mouse.up() + await expect + .poll(async () => { + const position = await transform(node) + return Math.hypot(position.x - nodeBefore.x, position.y - nodeBefore.y) + }) + .toBeGreaterThan(1) + expect(await transform(viewport)).toEqual(afterZoom) const data = { - selectedBlock: (await block.count()) ? await block.textContent() : undefined, - dialogTextLength: (await dialog.textContent())?.length, + beforePan, + afterPan, + afterZoom, + nodeBefore, + nodeAfter: await transform(node), } await run.page.keyboard.press('Escape') await dialog.waitFor({ state: 'hidden' }) + await expect(trigger).toBeFocused() return { status: 'completed', feedbackMs, data } }) for (const route of [ diff --git a/apps/docs/scripts/test-navigation-e2e.ts b/apps/docs/scripts/test-navigation-e2e.ts index b919d90b472..516311b1f94 100644 --- a/apps/docs/scripts/test-navigation-e2e.ts +++ b/apps/docs/scripts/test-navigation-e2e.ts @@ -608,6 +608,26 @@ try { } }) } + for (const kind of ['loop', 'parallel']) { + await check(`Inline ${kind} body opens lightbox at ${width}px`, async () => { + const page = await context.newPage() + try { + await open(page, `/workflows/blocks/${kind}`) + const container = page.locator('.react-flow__node-previewContainer').first() + await expect(container).toBeVisible() + const bounds = await container.boundingBox() + if (!bounds) throw new Error('Container has no visible bounds') + await container.click({ position: { x: bounds.width / 2, y: bounds.height - 8 } }) + const viewer = page.getByRole('dialog') + await expect(viewer).toBeVisible() + await page.screenshot({ path: join(directory, `${kind}-body-${width}.png`) }) + await page.keyboard.press('Escape') + await expect(viewer).toHaveCount(0) + } finally { + await page.close() + } + }) + } await check(`Workflow lightbox at ${width}px`, async () => { const page = await context.newPage() try { diff --git a/apps/sim/scripts/test-organization-search-e2e.ts b/apps/sim/scripts/test-organization-search-e2e.ts index c5a45689522..02a7a946042 100644 --- a/apps/sim/scripts/test-organization-search-e2e.ts +++ b/apps/sim/scripts/test-organization-search-e2e.ts @@ -788,6 +788,29 @@ try { ) assert.equal(response.status(), 404) }) + await check('A streamed denied chat remains navigable after hydration', async () => { + const releaseScripts = createDeferred() + const scripts = '**/_next/static/**/*.js' + await page.route(scripts, async (route) => { + await releaseScripts.promise + await route.continue() + }) + try { + const response = await page.goto(`${baseUrl.origin}${root}/search/chat/${deniedChatId}`, { + waitUntil: 'commit', + }) + assert(response, 'The denied chat navigation must return a document') + await response.body() + } finally { + releaseScripts.resolve() + await page.unrouteAll({ behavior: 'wait' }) + } + await page.waitForLoadState() + await expect(page.getByRole('heading', { name: 'Page not found' })).toBeVisible() + await page.getByRole('link', { name: 'Return home', exact: true }).click() + await expect(page).toHaveURL(`${baseUrl.origin}${root}/home`) + await expect(page.getByRole('textbox', { name: 'Ask Sim' })).toBeVisible() + }) await check('Both search modes fit desktop and 320px/390px phones', async () => { for (const width of [1440, 390, 320]) { await page.setViewportSize({ width, height: width === 1440 ? 1000 : 844 })