refactor(web): migrate app icon commands (#40646)

This commit is contained in:
yyh
2026-08-13 06:44:00 +00:00
committed by GitHub
parent d0e2723e45
commit 7ab368f4a4
22 changed files with 236 additions and 98 deletions
+25
View File
@@ -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() }}
+2
View File
@@ -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}!',
-8
View File
@@ -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
+12
View File
@@ -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)
@@ -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) => {
<>
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger
aria-label={t(($) => $['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"
>
<span aria-hidden className="i-ri-more-fill size-4" />
</DropdownMenuTrigger>
render={
<IconButton
aria-label={t(($) => $['operation.more'], { ns: 'common' })}
className="rounded-md data-popup-open:bg-state-base-hover data-popup-open:text-text-secondary"
>
<span aria-hidden className="i-ri-more-fill size-4" />
</IconButton>
}
/>
<DropdownMenuContent placement="bottom-end" sideOffset={4} popupClassName="w-[180px] p-1">
{canCreateAndModifySnippet && (
<>
@@ -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<Props> = ({
/>
</div>
<div className="flex items-center space-x-2">
<Button
<IconButton
aria-label={t(($) => $['operation.copy'], { ns: 'common' })}
className="px-2"
variant="secondary"
size="lg"
onClick={() => {
copy(current.modified)
toast.success(t(($) => $['actionMsg.copySuccessfully'], { ns: 'common' }))
}}
>
<RiClipboardLine aria-hidden="true" className="size-4 text-text-secondary" />
</Button>
<span aria-hidden="true" className="i-ri-clipboard-line size-4 text-text-secondary" />
</IconButton>
<Button variant="primary" onClick={onApply}>
{t(($) => $['generate.apply'], { ns: 'appDebug' })}
</Button>
@@ -13,6 +13,7 @@ import {
} from '@langgenius/dify-ui/dialog'
import { Field, FieldControl, FieldError, FieldLabel } from '@langgenius/dify-ui/field'
import { Form } from '@langgenius/dify-ui/form'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { Kbd, KbdGroup } from '@langgenius/dify-ui/kbd'
import { Tabs, TabsList, TabsPanel, TabsTab } from '@langgenius/dify-ui/tabs'
import { toast } from '@langgenius/dify-ui/toast'
@@ -259,16 +260,16 @@ function CreateFromDSLModal({
<DialogTitle className="title-2xl-semi-bold text-text-primary">
{t(($) => $.importApp, { ns: 'app' })}
</DialogTitle>
<Button
<IconButton
variant="ghost"
size="small"
size="lg"
aria-label={t(($) => $['operation.cancel'], { ns: 'common' })}
className="size-8 p-0"
className="rounded-md"
disabled={isImporting}
onClick={onClose}
>
<span aria-hidden className="i-ri-close-line size-5 text-text-tertiary" />
</Button>
</IconButton>
</div>
<Form<ImportFormValues> ref={formRef} onFormSubmit={handleSubmit}>
<Tabs value={currentTab} onValueChange={handleTabChange}>
@@ -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({
</div>
</div>
<div className="flex items-center pr-3 opacity-0 group-focus-within:opacity-100 group-hover:opacity-100">
<Button
<IconButton
ref={removeButtonRef}
variant="ghost"
size="small"
className="size-6 p-0"
size="md"
aria-label={t(($) => $['operation.delete'], { ns: 'common' })}
disabled={disabled}
onClick={removeFile}
>
<span aria-hidden className="i-ri-delete-bin-line size-4 text-text-tertiary" />
</Button>
</IconButton>
</div>
</div>
)}
@@ -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({
<DropdownMenu modal={false}>
<DropdownMenuTrigger
render={
<Button
size="small"
<IconButton
size="md"
variant="secondary"
aria-label={`${row.environment.display_name} · ${t(($) => $['deployTab.moreActions'])}`}
className="w-6 shrink-0 px-0"
className="shrink-0"
>
<span aria-hidden className="i-ri-more-fill size-4" />
</Button>
</IconButton>
}
/>
<DropdownMenuContent placement="bottom-end" sideOffset={4} popupClassName="w-50">
@@ -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(<FileImageItem file={file} canPreview showDownloadAction />)
const preview = screen.getByRole('button', { name: 'common.operation.view photo.png' })
await preview.hover()
await preview.click()
await expect.element(page.getByRole('dialog')).toBeVisible()
})
})
@@ -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(<FileImageItem file={createFile()} showDeleteAction showDownloadAction />)
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(<FileImageItem file={createFile()} showDownloadAction />)
@@ -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 = (
<FileImageRender
className="h-17 w-17 shadow-md"
imageUrl={base64Url || url || ''}
showDownloadAction={showDownloadAction}
/>
)
return (
<>
<div
className="group/file-image relative cursor-pointer"
onClick={() => canPreview && setImagePreviewUrl(base64Url || url || '')}
>
{showDeleteAction && (
<Button
aria-label={t(($) => $['operation.remove'], { ns: 'common' })}
className="absolute -top-1.5 -right-1.5 z-11 hidden size-5 rounded-full p-0 group-hover/file-image:flex"
onClick={(e) => {
e.stopPropagation()
onRemove?.(id)
}}
<div className="group/file-image relative">
{canPreview ? (
<button
type="button"
aria-label={`${t(($) => $['operation.view'], { ns: 'common' })} ${name}`}
className="block border-0 bg-transparent p-0 focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
onClick={() => setImagePreviewUrl(base64Url || url || '')}
>
<RiCloseLine
className="size-4 text-components-button-secondary-text"
{image}
</button>
) : (
image
)}
{showDeleteAction && (
<IconButton
aria-label={t(($) => $['operation.remove'], { ns: 'common' })}
variant="secondary"
size="sm"
className="pointer-events-none absolute -top-1.5 -right-1.5 z-11 rounded-full opacity-0 group-focus-within/file-image:pointer-events-auto group-focus-within/file-image:opacity-100 group-hover/file-image:pointer-events-auto group-hover/file-image:opacity-100"
onClick={() => onRemove?.(id)}
>
<span
aria-hidden="true"
className="i-ri-close-line size-4 text-components-button-secondary-text"
/>
</Button>
</IconButton>
)}
<FileImageRender
className="h-17 w-17 shadow-md"
imageUrl={base64Url || url || ''}
showDownloadAction={showDownloadAction}
/>
{progress >= 0 && !fileIsUploaded(file) && (
<div className="absolute inset-0 z-10 flex items-center justify-center border-2 border-effects-image-frame bg-background-overlay-alt">
<ProgressCircle
@@ -68,32 +76,28 @@ const FileImageItem = ({
)}
{progress === -1 && (
<div className="absolute inset-0 z-10 flex items-center justify-center border-2 border-state-destructive-border bg-background-overlay-destructive">
<button
type="button"
<IconButton
size="sm"
aria-label={t(($) => $['operation.retry'], { ns: 'common' })}
className="size-5 border-none bg-transparent p-0"
onClick={(e) => {
e.stopPropagation()
onReUpload?.(id)
}}
className="rounded-none hover:bg-transparent"
onClick={() => onReUpload?.(id)}
>
<ReplayLine className="size-5" aria-hidden="true" />
</button>
<span aria-hidden="true" className="i-custom-vender-other-replay-line size-5" />
</IconButton>
</div>
)}
{showDownloadAction && (
<div className="absolute inset-0.5 z-10 hidden bg-background-overlay-alt group-hover/file-image:block">
<button
type="button"
<div className="pointer-events-none absolute inset-0.5 z-10 bg-background-overlay-alt opacity-0 group-focus-within/file-image:opacity-100 group-hover/file-image:opacity-100">
<IconButton
size="md"
aria-label={t(($) => $['operation.download'], { ns: 'common' })}
className="absolute right-0.5 bottom-0.5 flex size-6 items-center justify-center rounded-lg border-none bg-components-actionbar-bg p-0 shadow-md"
onClick={(e) => {
e.stopPropagation()
className="pointer-events-none absolute right-0.5 bottom-0.5 rounded-lg bg-components-actionbar-bg shadow-md group-focus-within/file-image:pointer-events-auto group-hover/file-image:pointer-events-auto hover:bg-components-actionbar-bg"
onClick={() => {
downloadUrl({ url: download_url || '', fileName: name, target: '_blank' })
}}
>
<RiDownloadLine className="size-4 text-text-tertiary" aria-hidden="true" />
</button>
<span aria-hidden="true" className="i-ri-download-line size-4 text-text-tertiary" />
</IconButton>
</div>
)}
</div>
@@ -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 && (
<Button
<IconButton
aria-label={t(($) => $['operation.remove'], { ns: 'common' })}
className="absolute -top-1.5 -right-1.5 z-11 hidden size-5 rounded-full p-0 group-hover/file-item:flex"
variant="secondary"
size="sm"
className="absolute -top-1.5 -right-1.5 z-11 hidden rounded-full group-hover/file-item:flex"
onClick={() => onRemove?.(id)}
>
<span
className="i-ri-close-line size-4 text-components-button-secondary-text"
aria-hidden="true"
/>
</Button>
</IconButton>
)}
<div
className="mb-1 line-clamp-2 h-8 cursor-pointer system-xs-medium break-all text-text-tertiary"
@@ -1,6 +1,6 @@
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { DialogDescription, DialogTitle } from '@langgenius/dify-ui/dialog'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { DifyLogo } from '../../base/logo/dify-logo'
@@ -32,14 +32,15 @@ const Header = ({ onClose }: HeaderProps) => {
<DialogDescription className="system-sm-regular text-text-tertiary">
{t(($) => $['plansCommon.title.description'], { ns: 'billing' })}
</DialogDescription>
<Button
<IconButton
variant="secondary"
className="absolute -right-4.5 bottom-[40.5px] z-10 size-9 rounded-full p-2"
size="xl"
className="absolute -right-4.5 bottom-[40.5px] z-10 rounded-full"
aria-label={t(($) => $['operation.close'], { ns: 'common' })}
onClick={onClose}
>
<span aria-hidden="true" className="i-ri-close-line size-5" />
</Button>
</IconButton>
</div>
</div>
)
@@ -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 (
<MenuDialog show onClose={onCancelAction}>
<div className="fixed top-6 right-6 z-20 flex shrink-0 flex-col items-center">
<Button
<IconButton
variant="tertiary"
size="large"
className="px-2"
size="xl"
aria-label={t(($) => $['operation.close'], { ns: 'common' })}
onClick={onCancelAction}
>
<span className="i-ri-close-line size-5" />
</Button>
</IconButton>
<div className="mt-1 system-2xs-medium-uppercase text-text-tertiary">ESC</div>
</div>
<div className="flex h-screen w-full max-w-full pl-0 sm:pl-58">
+4 -5
View File
@@ -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}
/>
<div className="fixed top-6 right-6 flex shrink-0 flex-col items-center">
<Button
<IconButton
variant="tertiary"
size="large"
className="px-2"
size="xl"
aria-label={t(($) => $['operation.close'], { ns: 'common' })}
onClick={onCancel}
>
<span className="i-ri-close-line h-5 w-5" />
</Button>
</IconButton>
<div className="mt-1 system-2xs-medium-uppercase text-text-tertiary">ESC</div>
</div>
</div>
@@ -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}
</Button>
<Button
<IconButton
ref={closeButtonRef}
variant="ghost"
size="small"
className="size-6 px-0 text-text-tertiary hover:text-text-secondary"
size="md"
aria-label={minimizeLabel}
onClick={onMinimize}
>
<span aria-hidden className="i-ri-arrow-left-down-line size-4" />
</Button>
</IconButton>
</>
)}
</div>
@@ -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"
/>
<div className="absolute inset-y-0 right-0 flex items-center">
<Button
type="button"
variant="ghost"
<IconButton
size="lg"
aria-label={t(($) => $[showPassword ? 'hidePassword' : 'showPassword'], {
ns: 'login',
})}
className="mr-1 size-8 p-0 text-text-tertiary hover:text-text-secondary"
className="mr-1"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
@@ -164,7 +164,7 @@ export default function MailAndPasswordAuth({ isInvite, isEmailSetup }: MailAndP
) : (
<span className="i-ri-eye-line size-4" aria-hidden="true" />
)}
</Button>
</IconButton>
</div>
</div>
</Field>
+4
View File
@@ -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": [
+2
View File
@@ -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'],
+28
View File
@@ -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,
},
},
})
+17
View File
@@ -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<typeof import('react-i18next')>('react-i18next')
const { createReactI18nextMock } = await import('./test/i18n-mock')
return {
...actual,
...createReactI18nextMock(),
}
})