From 7ab368f4a4b968f60669d0136a8f0fc28da87680 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Thu, 13 Aug 2026 06:44:00 +0000 Subject: [PATCH] refactor(web): migrate app icon commands (#40646) --- .github/workflows/web-tests.yml | 25 ++++++ knip.config.ts | 2 + oxlint-suppressions.json | 8 -- pnpm-lock.yaml | 12 +++ .../app-sidebar/snippet-info/dropdown.tsx | 15 ++-- .../configuration/config/automatic/result.tsx | 11 +-- .../app/create-from-dsl-modal/index.tsx | 9 +- .../app/create-from-dsl-modal/uploader.tsx | 8 +- .../deploy/environment-table/row-actions.tsx | 9 +- .../file-image-item.browser.spec.tsx | 33 ++++++++ .../__tests__/file-image-item.spec.tsx | 12 +++ .../file-image-item.tsx | 84 ++++++++++--------- .../file-uploader-in-chat-input/file-item.tsx | 9 +- web/app/components/billing/pricing/header.tsx | 9 +- .../header/account-setting/index.tsx | 9 +- web/app/components/integrations/modal.tsx | 9 +- .../step-by-step-tour/floating-widget.tsx | 9 +- .../components/mail-and-password-auth.tsx | 10 +-- web/package.json | 4 + web/vite.config.ts | 2 + web/vitest.browser.config.ts | 28 +++++++ web/vitest.browser.setup.ts | 17 ++++ 22 files changed, 236 insertions(+), 98 deletions(-) create mode 100644 web/app/components/base/file-uploader/file-uploader-in-chat-input/__tests__/file-image-item.browser.spec.tsx create mode 100644 web/vitest.browser.config.ts create mode 100644 web/vitest.browser.setup.ts diff --git a/.github/workflows/web-tests.yml b/.github/workflows/web-tests.yml index c57f2cfb56b..27b9ee9005f 100644 --- a/.github/workflows/web-tests.yml +++ b/.github/workflows/web-tests.yml @@ -44,6 +44,31 @@ jobs: include-hidden-files: true retention-days: 1 + browser-test: + name: Web Browser Tests + runs-on: depot-ubuntu-24.04-4 + timeout-minutes: 20 + defaults: + run: + shell: bash + working-directory: ./web + + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup web environment + uses: ./.github/actions/setup-web + + - name: Install Chromium for Browser Mode + timeout-minutes: 15 + run: vp exec playwright install --with-deps --only-shell chromium + + - name: Run browser tests + run: vp test run --config vitest.browser.config.ts --silent=passed-only + merge-reports: name: Merge Test Reports if: ${{ !cancelled() }} diff --git a/knip.config.ts b/knip.config.ts index 63e60aeab63..46d5ecc3573 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -15,6 +15,8 @@ const config: KnipConfig = { 'tsslint.config.ts', 'dev-proxy.config.ts', 'plugins/eslint/index.js', + 'vitest.browser.config.ts', + 'vitest.browser.setup.ts', ], project: [ '**/*.{js,mjs,cjs,jsx,ts,tsx,mts,cts,css,mdx}!', diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index 40c8240e344..df14b5896a8 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -979,14 +979,6 @@ "count": 1 } }, - "web/app/components/base/file-uploader/file-uploader-in-chat-input/file-image-item.tsx": { - "jsx_a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx_a11y/no-static-element-interactions": { - "count": 1 - } - }, "web/app/components/base/file-uploader/file-uploader-in-chat-input/file-item.tsx": { "jsx_a11y/click-events-have-key-events": { "count": 1 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2334544b137..0296daa46b5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1594,6 +1594,12 @@ importers: '@vitejs/plugin-rsc': specifier: 'catalog:' version: 0.5.33(@voidzero-dev/vite-plus-core@0.2.8(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.8)(yaml@2.9.0))(react-dom@19.2.8(react@19.2.8))(react-server-dom-webpack@19.2.8(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8) + '@vitest/browser': + specifier: 'catalog:' + version: 4.1.10(@voidzero-dev/vite-plus-core@0.2.8(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.8)(yaml@2.9.0))(vitest@4.1.10) + '@vitest/browser-playwright': + specifier: 'catalog:' + version: 4.1.10(@voidzero-dev/vite-plus-core@0.2.8(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.8)(yaml@2.9.0))(playwright@1.62.1)(vitest@4.1.10) '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.10(@vitest/browser@4.1.10)(vitest@4.1.10) @@ -1606,6 +1612,9 @@ importers: happy-dom: specifier: 'catalog:' version: 20.11.1 + playwright: + specifier: 'catalog:' + version: 1.62.1 postcss: specifier: 'catalog:' version: 8.5.25 @@ -1642,6 +1651,9 @@ importers: vitest: specifier: 'catalog:' version: 4.1.10(@types/node@25.9.5)(@vitest/browser-playwright@4.1.10)(@vitest/browser-preview@4.1.10)(@vitest/coverage-v8@4.1.10)(@voidzero-dev/vite-plus-core@0.2.8(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.8)(yaml@2.9.0))(happy-dom@20.11.1) + vitest-browser-react: + specifier: 'catalog:' + version: 2.2.0(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.10) vitest-canvas-mock: specifier: 'catalog:' version: 1.1.4(vitest@4.1.10) diff --git a/web/app/components/app-sidebar/snippet-info/dropdown.tsx b/web/app/components/app-sidebar/snippet-info/dropdown.tsx index 1dfee4a5bf8..321bbde571e 100644 --- a/web/app/components/app-sidebar/snippet-info/dropdown.tsx +++ b/web/app/components/app-sidebar/snippet-info/dropdown.tsx @@ -17,6 +17,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' +import { IconButton } from '@langgenius/dify-ui/icon-button' import { toast } from '@langgenius/dify-ui/toast' import { useAtomValue } from 'jotai' import * as React from 'react' @@ -127,11 +128,15 @@ const SnippetInfoDropdown = ({ snippet }: SnippetInfoDropdownProps) => { <> $['operation.more'], { ns: 'common' })} - className="action-btn action-btn-m size-6 rounded-md text-text-tertiary data-popup-open:bg-state-base-hover data-popup-open:text-text-secondary" - > - - + render={ + $['operation.more'], { ns: 'common' })} + className="rounded-md data-popup-open:bg-state-base-hover data-popup-open:text-text-secondary" + > + + + } + /> {canCreateAndModifySnippet && ( <> diff --git a/web/app/components/app/configuration/config/automatic/result.tsx b/web/app/components/app/configuration/config/automatic/result.tsx index eea5ee9c717..556cb9dd97c 100644 --- a/web/app/components/app/configuration/config/automatic/result.tsx +++ b/web/app/components/app/configuration/config/automatic/result.tsx @@ -2,7 +2,7 @@ import type { FC } from 'react' import type { GenRes } from '@/service/debug' import { Button } from '@langgenius/dify-ui/button' -import { RiClipboardLine } from '@remixicon/react' +import { IconButton } from '@langgenius/dify-ui/icon-button' import copy from 'copy-to-clipboard' import * as React from 'react' import { useTranslation } from 'react-i18next' @@ -52,16 +52,17 @@ const Result: FC = ({ />
- +
ref={formRef} onFormSubmit={handleSubmit}> diff --git a/web/app/components/app/create-from-dsl-modal/uploader.tsx b/web/app/components/app/create-from-dsl-modal/uploader.tsx index f54d740d0a4..448e07ed7e5 100644 --- a/web/app/components/app/create-from-dsl-modal/uploader.tsx +++ b/web/app/components/app/create-from-dsl-modal/uploader.tsx @@ -2,6 +2,7 @@ import type { ChangeEvent, DragEvent, MouseEvent, RefObject } from 'react' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' +import { IconButton } from '@langgenius/dify-ui/icon-button' import { toast } from '@langgenius/dify-ui/toast' import { useId, useLayoutEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -191,17 +192,16 @@ export function Uploader({
- +
)} diff --git a/web/app/components/app/deploy/environment-table/row-actions.tsx b/web/app/components/app/deploy/environment-table/row-actions.tsx index 8f4d8097498..40af27d900b 100644 --- a/web/app/components/app/deploy/environment-table/row-actions.tsx +++ b/web/app/components/app/deploy/environment-table/row-actions.tsx @@ -10,6 +10,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' +import { IconButton } from '@langgenius/dify-ui/icon-button' import { Fragment, useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { getWorkflowVersionName } from '@/app/components/workflow/utils/version' @@ -136,14 +137,14 @@ export function EnvironmentRowActions({ $['deployTab.moreActions'])}`} - className="w-6 shrink-0 px-0" + className="shrink-0" > - + } /> diff --git a/web/app/components/base/file-uploader/file-uploader-in-chat-input/__tests__/file-image-item.browser.spec.tsx b/web/app/components/base/file-uploader/file-uploader-in-chat-input/__tests__/file-image-item.browser.spec.tsx new file mode 100644 index 00000000000..7e7d4f8e88b --- /dev/null +++ b/web/app/components/base/file-uploader/file-uploader-in-chat-input/__tests__/file-image-item.browser.spec.tsx @@ -0,0 +1,33 @@ +import type { FileEntity } from '../../types' +import { page } from 'vite-plus/test/browser' +import { render } from 'vitest-browser-react' +import FileImageItem from '../file-image-item' + +vi.mock('@/utils/download', () => ({ + downloadUrl: vi.fn(), +})) + +const file: FileEntity = { + id: 'file-1', + name: 'photo.png', + size: 4096, + type: 'image/png', + progress: 100, + transferMethod: 'local_file', + supportFileType: 'image', + uploadedId: 'uploaded-1', + base64Url: 'data:image/png;base64,abc', + url: 'https://example.com/photo.png', +} + +describe('FileImageItem pointer interaction', () => { + it('keeps the preview clickable when the download action is visible', async () => { + const screen = await render() + const preview = screen.getByRole('button', { name: 'common.operation.view photo.png' }) + + await preview.hover() + await preview.click() + + await expect.element(page.getByRole('dialog')).toBeVisible() + }) +}) diff --git a/web/app/components/base/file-uploader/file-uploader-in-chat-input/__tests__/file-image-item.spec.tsx b/web/app/components/base/file-uploader/file-uploader-in-chat-input/__tests__/file-image-item.spec.tsx index c9f0c5c2e3a..21d705ff866 100644 --- a/web/app/components/base/file-uploader/file-uploader-in-chat-input/__tests__/file-image-item.spec.tsx +++ b/web/app/components/base/file-uploader/file-uploader-in-chat-input/__tests__/file-image-item.spec.tsx @@ -1,5 +1,6 @@ import type { FileEntity } from '../../types' import { fireEvent, render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import { TransferMethod } from '@/types/app' import FileImageItem from '../file-image-item' @@ -123,6 +124,17 @@ describe('FileImageItem', () => { expect(screen.getByRole('button', { name: 'common.operation.download' })).toBeInTheDocument() }) + it('should keep image actions keyboard reachable', async () => { + const user = userEvent.setup() + render() + + await user.tab() + expect(screen.getByRole('button', { name: 'common.operation.remove' })).toHaveFocus() + + await user.tab() + expect(screen.getByRole('button', { name: 'common.operation.download' })).toHaveFocus() + }) + it('should call downloadUrl when download button is clicked', async () => { const { downloadUrl } = await import('@/utils/download') render() diff --git a/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-image-item.tsx b/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-image-item.tsx index 28861f7684a..f27abffc5b9 100644 --- a/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-image-item.tsx +++ b/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-image-item.tsx @@ -1,10 +1,8 @@ import type { FileEntity } from '../types' -import { Button } from '@langgenius/dify-ui/button' +import { IconButton } from '@langgenius/dify-ui/icon-button' import { ProgressCircle } from '@langgenius/dify-ui/progress' -import { RiCloseLine, RiDownloadLine } from '@remixicon/react' import { useState } from 'react' import { useTranslation } from 'react-i18next' -import { ReplayLine } from '@/app/components/base/icons/src/vender/other' import ImagePreview from '@/app/components/base/image-uploader/image-preview' import { downloadUrl } from '@/utils/download' import FileImageRender from '../file-image-render' @@ -30,33 +28,43 @@ const FileImageItem = ({ const { id, progress, base64Url, url, name } = file const [imagePreviewUrl, setImagePreviewUrl] = useState('') const download_url = url ? `${url}&as_attachment=true` : base64Url + const image = ( + + ) return ( <> -
canPreview && setImagePreviewUrl(base64Url || url || '')} - > - {showDeleteAction && ( - + )} - {progress >= 0 && !fileIsUploaded(file) && (
- +
)} {showDownloadAction && ( -
- +
)}
diff --git a/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-item.tsx b/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-item.tsx index 8a76382fa39..7ee4edae4dd 100644 --- a/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-item.tsx +++ b/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-item.tsx @@ -1,5 +1,4 @@ import type { FileEntity } from '../types' -import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { IconButton } from '@langgenius/dify-ui/icon-button' import { ProgressCircle } from '@langgenius/dify-ui/progress' @@ -53,16 +52,18 @@ const FileItem = ({ )} > {showDeleteAction && ( - + )}
{ {t(($) => $['plansCommon.title.description'], { ns: 'billing' })} - +
) diff --git a/web/app/components/header/account-setting/index.tsx b/web/app/components/header/account-setting/index.tsx index 2635adae5d7..e7d4aa9ddfa 100644 --- a/web/app/components/header/account-setting/index.tsx +++ b/web/app/components/header/account-setting/index.tsx @@ -1,7 +1,7 @@ 'use client' import type { AccountSettingTab } from '@/app/components/header/account-setting/constants' -import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' +import { IconButton } from '@langgenius/dify-ui/icon-button' import { ScrollArea, ScrollAreaContent, @@ -173,15 +173,14 @@ export default function AccountSetting({ return (
- +
ESC
diff --git a/web/app/components/integrations/modal.tsx b/web/app/components/integrations/modal.tsx index f203913abbf..a7e1b5f3c5d 100644 --- a/web/app/components/integrations/modal.tsx +++ b/web/app/components/integrations/modal.tsx @@ -1,7 +1,7 @@ 'use client' import type { IntegrationSection } from './routes' -import { Button } from '@langgenius/dify-ui/button' +import { IconButton } from '@langgenius/dify-ui/icon-button' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import MenuDialog from '@/app/components/header/account-setting/menu-dialog' @@ -38,15 +38,14 @@ export default function IntegrationsSettingModal({ onSwitchToMarketplace={handleSwitchToMarketplace} />
- +
ESC
diff --git a/web/app/components/step-by-step-tour/floating-widget.tsx b/web/app/components/step-by-step-tour/floating-widget.tsx index 69cdc322fd5..9d244d304ee 100644 --- a/web/app/components/step-by-step-tour/floating-widget.tsx +++ b/web/app/components/step-by-step-tour/floating-widget.tsx @@ -4,6 +4,7 @@ import type { ComponentProps, RefObject } from 'react' import type { StepByStepTourTaskId, StepByStepTourTaskView } from './types' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' +import { IconButton } from '@langgenius/dify-ui/icon-button' import { PopoverDescription, PopoverTitle } from '@langgenius/dify-ui/popover' import { useEffect } from 'react' @@ -80,16 +81,14 @@ export function FloatingChecklist({ > {skipLabel} - + )} diff --git a/web/app/signin/components/mail-and-password-auth.tsx b/web/app/signin/components/mail-and-password-auth.tsx index 8e223955b7d..10363cbe1b5 100644 --- a/web/app/signin/components/mail-and-password-auth.tsx +++ b/web/app/signin/components/mail-and-password-auth.tsx @@ -1,6 +1,7 @@ import { Button } from '@langgenius/dify-ui/button' import { Field, FieldControl, FieldLabel } from '@langgenius/dify-ui/field' import { Form } from '@langgenius/dify-ui/form' +import { IconButton } from '@langgenius/dify-ui/icon-button' import { toast } from '@langgenius/dify-ui/toast' import { useQueryClient } from '@tanstack/react-query' import { useState } from 'react' @@ -150,13 +151,12 @@ export default function MailAndPasswordAuth({ isInvite, isEmailSetup }: MailAndP className="pr-10" />
- +
diff --git a/web/package.json b/web/package.json index 74827b86870..482b62d7309 100644 --- a/web/package.json +++ b/web/package.json @@ -181,10 +181,13 @@ "@typescript/native": "catalog:", "@vitejs/plugin-react": "catalog:", "@vitejs/plugin-rsc": "catalog:", + "@vitest/browser": "catalog:", + "@vitest/browser-playwright": "catalog:", "@vitest/coverage-v8": "catalog:", "agentation": "catalog:", "code-inspector-plugin": "catalog:", "happy-dom": "catalog:", + "playwright": "catalog:", "postcss": "catalog:", "react-server-dom-webpack": "catalog:", "storybook": "catalog:", @@ -197,6 +200,7 @@ "vite-plugin-inspect": "catalog:", "vite-plus": "catalog:", "vitest": "catalog:", + "vitest-browser-react": "catalog:", "vitest-canvas-mock": "catalog:" }, "browserslist": [ diff --git a/web/vite.config.ts b/web/vite.config.ts index c7354b76ec1..935eee44c3a 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -1,5 +1,6 @@ import { fileURLToPath } from 'node:url' import { defineConfig, lazyPlugins } from 'vite-plus' +import { configDefaults } from 'vitest/config' import { createCodeInspectorPlugin, createForceInspectorClientInjectionPlugin, @@ -95,6 +96,7 @@ export default defineConfig(({ mode }) => { environment: 'happy-dom', globals: true, setupFiles: ['./vitest.setup.ts'], + exclude: [...configDefaults.exclude, '**/*.browser.spec.{ts,tsx}'], coverage: { provider: 'v8', reporter: isCI ? ['json', 'json-summary'] : ['text', 'json', 'json-summary'], diff --git a/web/vitest.browser.config.ts b/web/vitest.browser.config.ts new file mode 100644 index 00000000000..df0a9361a78 --- /dev/null +++ b/web/vitest.browser.config.ts @@ -0,0 +1,28 @@ +import tailwindcss from '@tailwindcss/vite' +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite-plus' +import { playwright } from 'vite-plus/test/browser-playwright' + +export default defineConfig({ + define: { + 'process.env': '{}', + }, + plugins: [tailwindcss(), react()], + resolve: { + tsconfigPaths: true, + }, + optimizeDeps: { + include: ['vite-plus/test/browser'], + }, + test: { + globals: true, + setupFiles: ['./vitest.browser.setup.ts'], + include: ['app/**/*.browser.spec.{ts,tsx}'], + browser: { + enabled: true, + provider: playwright(), + instances: [{ browser: 'chromium' }], + headless: true, + }, + }, +}) diff --git a/web/vitest.browser.setup.ts b/web/vitest.browser.setup.ts new file mode 100644 index 00000000000..eb6c16349a3 --- /dev/null +++ b/web/vitest.browser.setup.ts @@ -0,0 +1,17 @@ +import { vi } from 'vitest' +import './app/styles/globals.css' + +document.documentElement.dataset.theme = 'light' + +;( + globalThis as typeof globalThis & { BASE_UI_ANIMATIONS_DISABLED: boolean } +).BASE_UI_ANIMATIONS_DISABLED = true + +vi.mock('react-i18next', async () => { + const actual = await vi.importActual('react-i18next') + const { createReactI18nextMock } = await import('./test/i18n-mock') + return { + ...actual, + ...createReactI18nextMock(), + } +})