mirror of
https://github.com/langgenius/dify.git
synced 2024-11-16 03:32:23 +08:00
feat: add frontend unit test framework (#6426)
This commit is contained in:
parent
afe95fa780
commit
4ae4895ebe
6
web/.vscode/extensions.json
vendored
Normal file
6
web/.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"firsttris.vscode-jest-runner"
|
||||
]
|
||||
}
|
|
@ -74,6 +74,25 @@ npm run start --port=3001 --host=0.0.0.0
|
|||
|
||||
If your IDE is VSCode, rename `web/.vscode/settings.example.json` to `web/.vscode/settings.json` for lint code setting.
|
||||
|
||||
## Test
|
||||
|
||||
We start to use [Jest](https://jestjs.io/) and [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) for Unit Testing.
|
||||
|
||||
You can create a test file with a suffix of `.spec` beside the file that to be tested. For example, if you want to test a file named `util.ts`. The test file name should be `util.spec.ts`.
|
||||
|
||||
Run test:
|
||||
|
||||
```bash
|
||||
npm run test
|
||||
```
|
||||
|
||||
If you are not familiar with writing tests, here is some code to refer to:
|
||||
* [classnames.spec.ts](./utils/classnames.spec.ts)
|
||||
* [index.spec.tsx](./app/components/base/button/index.spec.tsx)
|
||||
|
||||
|
||||
|
||||
|
||||
## Documentation
|
||||
|
||||
Visit <https://docs.dify.ai/getting-started/readme> to view the full documentation.
|
||||
|
|
49
web/app/components/base/button/index.spec.tsx
Normal file
49
web/app/components/base/button/index.spec.tsx
Normal file
|
@ -0,0 +1,49 @@
|
|||
import React from 'react'
|
||||
import { cleanup, fireEvent, render } from '@testing-library/react'
|
||||
import Button from './index'
|
||||
|
||||
afterEach(cleanup)
|
||||
// https://testing-library.com/docs/queries/about
|
||||
describe('Button text', () => {
|
||||
test('Button text should be same as children', async () => {
|
||||
const { getByRole, container } = render(<Button>Click me</Button>)
|
||||
expect(getByRole('button').textContent).toBe('Click me')
|
||||
expect(container.querySelector('button')?.textContent).toBe('Click me')
|
||||
})
|
||||
|
||||
test('Loading button text should include same as children', async () => {
|
||||
const { getByRole } = render(<Button loading>Click me</Button>)
|
||||
expect(getByRole('button').textContent?.includes('Loading')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Button style', () => {
|
||||
test('Button should have default variant', async () => {
|
||||
const { getByRole } = render(<Button>Click me</Button>)
|
||||
expect(getByRole('button').className).toContain('btn-secondary')
|
||||
})
|
||||
|
||||
test('Button should have primary variant', async () => {
|
||||
const { getByRole } = render(<Button variant='primary'>Click me</Button>)
|
||||
expect(getByRole('button').className).toContain('btn-primary')
|
||||
})
|
||||
|
||||
test('Button should have warning variant', async () => {
|
||||
const { getByRole } = render(<Button variant='warning'>Click me</Button>)
|
||||
expect(getByRole('button').className).toContain('btn-warning')
|
||||
})
|
||||
|
||||
test('Button disabled should have disabled variant', async () => {
|
||||
const { getByRole } = render(<Button disabled>Click me</Button>)
|
||||
expect(getByRole('button').className).toContain('btn-disabled')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Button events', () => {
|
||||
test('onClick should been call after clicked', async () => {
|
||||
const onClick = jest.fn()
|
||||
const { getByRole } = render(<Button onClick={onClick}>Click me</Button>)
|
||||
fireEvent.click(getByRole('button'))
|
||||
expect(onClick).toHaveBeenCalled()
|
||||
})
|
||||
})
|
|
@ -45,7 +45,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||
{...props}
|
||||
>
|
||||
{children}
|
||||
<Spinner loading={loading} className='!text-white !h-3 !w-3 !border-2 !ml-1' />
|
||||
{loading && <Spinner loading={loading} className='!text-white !h-3 !w-3 !border-2 !ml-1' />}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
|
|
208
web/jest.config.ts
Normal file
208
web/jest.config.ts
Normal file
|
@ -0,0 +1,208 @@
|
|||
/**
|
||||
* For a detailed explanation regarding each configuration property, visit:
|
||||
* https://jestjs.io/docs/configuration
|
||||
*/
|
||||
|
||||
import type { Config } from 'jest'
|
||||
import nextJest from 'next/jest.js'
|
||||
|
||||
// https://nextjs.org/docs/app/building-your-application/testing/jest
|
||||
const createJestConfig = nextJest({
|
||||
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
|
||||
dir: './',
|
||||
})
|
||||
|
||||
const config: Config = {
|
||||
// All imported modules in your tests should be mocked automatically
|
||||
// automock: false,
|
||||
|
||||
// Stop running tests after `n` failures
|
||||
// bail: 0,
|
||||
|
||||
// The directory where Jest should store its cached dependency information
|
||||
// cacheDirectory: "/private/var/folders/9c/7gly5yl90qxdjljqsvkk758h0000gn/T/jest_dx",
|
||||
|
||||
// Automatically clear mock calls, instances, contexts and results before every test
|
||||
clearMocks: true,
|
||||
|
||||
// Indicates whether the coverage information should be collected while executing the test
|
||||
collectCoverage: false,
|
||||
|
||||
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
||||
// collectCoverageFrom: undefined,
|
||||
|
||||
// The directory where Jest should output its coverage files
|
||||
// coverageDirectory: "coverage",
|
||||
|
||||
// An array of regexp pattern strings used to skip coverage collection
|
||||
// coveragePathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// Indicates which provider should be used to instrument code for coverage
|
||||
coverageProvider: 'v8',
|
||||
|
||||
// A list of reporter names that Jest uses when writing coverage reports
|
||||
// coverageReporters: [
|
||||
// "json",
|
||||
// "text",
|
||||
// "lcov",
|
||||
// "clover"
|
||||
// ],
|
||||
|
||||
// An object that configures minimum threshold enforcement for coverage results
|
||||
// coverageThreshold: undefined,
|
||||
|
||||
// A path to a custom dependency extractor
|
||||
// dependencyExtractor: undefined,
|
||||
|
||||
// Make calling deprecated APIs throw helpful error messages
|
||||
// errorOnDeprecated: false,
|
||||
|
||||
// The default configuration for fake timers
|
||||
// fakeTimers: {
|
||||
// "enableGlobally": false
|
||||
// },
|
||||
|
||||
// Force coverage collection from ignored files using an array of glob patterns
|
||||
// forceCoverageMatch: [],
|
||||
|
||||
// A path to a module which exports an async function that is triggered once before all test suites
|
||||
// globalSetup: undefined,
|
||||
|
||||
// A path to a module which exports an async function that is triggered once after all test suites
|
||||
// globalTeardown: undefined,
|
||||
|
||||
// A set of global variables that need to be available in all test environments
|
||||
// globals: {},
|
||||
|
||||
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
|
||||
// maxWorkers: "50%",
|
||||
|
||||
// An array of directory names to be searched recursively up from the requiring module's location
|
||||
// moduleDirectories: [
|
||||
// "node_modules"
|
||||
// ],
|
||||
|
||||
// An array of file extensions your modules use
|
||||
// moduleFileExtensions: [
|
||||
// "js",
|
||||
// "mjs",
|
||||
// "cjs",
|
||||
// "jsx",
|
||||
// "ts",
|
||||
// "tsx",
|
||||
// "json",
|
||||
// "node"
|
||||
// ],
|
||||
|
||||
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
||||
moduleNameMapper: {
|
||||
'^@/components/(.*)$': '<rootDir>/components/$1',
|
||||
},
|
||||
|
||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||
// modulePathIgnorePatterns: [],
|
||||
|
||||
// Activates notifications for test results
|
||||
// notify: false,
|
||||
|
||||
// An enum that specifies notification mode. Requires { notify: true }
|
||||
// notifyMode: "failure-change",
|
||||
|
||||
// A preset that is used as a base for Jest's configuration
|
||||
// preset: undefined,
|
||||
|
||||
// Run tests from one or more projects
|
||||
// projects: undefined,
|
||||
|
||||
// Use this configuration option to add custom reporters to Jest
|
||||
// reporters: undefined,
|
||||
|
||||
// Automatically reset mock state before every test
|
||||
// resetMocks: false,
|
||||
|
||||
// Reset the module registry before running each individual test
|
||||
// resetModules: false,
|
||||
|
||||
// A path to a custom resolver
|
||||
// resolver: undefined,
|
||||
|
||||
// Automatically restore mock state and implementation before every test
|
||||
// restoreMocks: false,
|
||||
|
||||
// The root directory that Jest should scan for tests and modules within
|
||||
// rootDir: undefined,
|
||||
|
||||
// A list of paths to directories that Jest should use to search for files in
|
||||
// roots: [
|
||||
// "<rootDir>"
|
||||
// ],
|
||||
|
||||
// Allows you to use a custom runner instead of Jest's default test runner
|
||||
// runner: "jest-runner",
|
||||
|
||||
// The paths to modules that run some code to configure or set up the testing environment before each test
|
||||
// setupFiles: [],
|
||||
|
||||
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
||||
// setupFilesAfterEnv: [],
|
||||
|
||||
// The number of seconds after which a test is considered as slow and reported as such in the results.
|
||||
// slowTestThreshold: 5,
|
||||
|
||||
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
||||
// snapshotSerializers: [],
|
||||
|
||||
// The test environment that will be used for testing
|
||||
testEnvironment: 'jsdom',
|
||||
|
||||
// Options that will be passed to the testEnvironment
|
||||
// testEnvironmentOptions: {},
|
||||
|
||||
// Adds a location field to test results
|
||||
// testLocationInResults: false,
|
||||
|
||||
// The glob patterns Jest uses to detect test files
|
||||
// testMatch: [
|
||||
// "**/__tests__/**/*.[jt]s?(x)",
|
||||
// "**/?(*.)+(spec|test).[tj]s?(x)"
|
||||
// ],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||
// testPathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// The regexp pattern or array of patterns that Jest uses to detect test files
|
||||
// testRegex: [],
|
||||
|
||||
// This option allows the use of a custom results processor
|
||||
// testResultsProcessor: undefined,
|
||||
|
||||
// This option allows use of a custom test runner
|
||||
// testRunner: "jest-circus/runner",
|
||||
|
||||
// A map from regular expressions to paths to transformers
|
||||
// transform: undefined,
|
||||
|
||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||
// transformIgnorePatterns: [
|
||||
// "/node_modules/",
|
||||
// "\\.pnp\\.[^\\/]+$"
|
||||
// ],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||
// unmockedModulePathPatterns: undefined,
|
||||
|
||||
// Indicates whether each individual test should be reported during the run
|
||||
// verbose: undefined,
|
||||
|
||||
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
|
||||
// watchPathIgnorePatterns: [],
|
||||
|
||||
// Whether to use watchman for file crawling
|
||||
// watchman: true,
|
||||
}
|
||||
|
||||
export default createJestConfig(config)
|
|
@ -15,7 +15,9 @@
|
|||
"prepare": "cd ../ && node -e \"if (process.env.NODE_ENV !== 'production'){process.exit(1)} \" || husky install ./web/.husky",
|
||||
"gen-icons": "node ./app/components/base/icons/script.js",
|
||||
"uglify-embed": "node ./bin/uglify-embed",
|
||||
"check-i18n": "node ./i18n/script.js"
|
||||
"check-i18n": "node ./i18n/script.js",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.22.3",
|
||||
|
@ -102,8 +104,12 @@
|
|||
"@antfu/eslint-config": "^0.36.0",
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@rgrove/parse-xml": "^4.1.0",
|
||||
"@testing-library/dom": "^10.3.2",
|
||||
"@testing-library/jest-dom": "^6.4.6",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/dagre": "^0.7.52",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/js-cookie": "^3.0.3",
|
||||
"@types/lodash-es": "^4.17.7",
|
||||
"@types/negotiator": "^0.6.1",
|
||||
|
@ -124,10 +130,13 @@
|
|||
"eslint": "^8.36.0",
|
||||
"eslint-config-next": "^14.0.4",
|
||||
"husky": "^8.0.3",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"lint-staged": "^13.2.2",
|
||||
"postcss": "^8.4.31",
|
||||
"sass": "^1.61.0",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "4.9.5",
|
||||
"uglify-js": "^3.17.4"
|
||||
},
|
||||
|
|
55
web/utils/classnames.spec.ts
Normal file
55
web/utils/classnames.spec.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import cn from './classnames'
|
||||
|
||||
describe('classnames', () => {
|
||||
test('classnames libs feature', () => {
|
||||
expect(cn('foo')).toBe('foo')
|
||||
expect(cn('foo', 'bar')).toBe('foo bar')
|
||||
expect(cn(['foo', 'bar'])).toBe('foo bar')
|
||||
|
||||
expect(cn(undefined)).toBe('')
|
||||
expect(cn(null)).toBe('')
|
||||
expect(cn(false)).toBe('')
|
||||
|
||||
expect(cn({
|
||||
foo: true,
|
||||
bar: false,
|
||||
baz: true,
|
||||
})).toBe('foo baz')
|
||||
})
|
||||
|
||||
test('tailwind-merge', () => {
|
||||
expect(cn('p-0')).toBe('p-0')
|
||||
expect(cn('text-right text-center text-left')).toBe('text-left')
|
||||
expect(cn('pl-4 p-8')).toBe('p-8')
|
||||
expect(cn('m-[2px] m-[4px]')).toBe('m-[4px]')
|
||||
expect(cn('m-1 m-[4px]')).toBe('m-[4px]')
|
||||
expect(cn('overflow-x-auto hover:overflow-x-hidden overflow-x-scroll')).toBe(
|
||||
'hover:overflow-x-hidden overflow-x-scroll',
|
||||
)
|
||||
expect(cn('h-10 h-min')).toBe('h-min')
|
||||
expect(cn('bg-grey-5 bg-hotpink')).toBe('bg-hotpink')
|
||||
|
||||
expect(cn('hover:block hover:inline')).toBe('hover:inline')
|
||||
|
||||
expect(cn('font-medium !font-bold')).toBe('font-medium !font-bold')
|
||||
expect(cn('!font-medium !font-bold')).toBe('!font-bold')
|
||||
|
||||
expect(cn('text-gray-100 text-primary-200')).toBe('text-primary-200')
|
||||
expect(cn('text-some-unknown-color text-components-input-bg-disabled text-primary-200')).toBe('text-primary-200')
|
||||
expect(cn('bg-some-unknown-color bg-components-input-bg-disabled bg-primary-200')).toBe('bg-primary-200')
|
||||
|
||||
expect(cn('border-t border-white/10')).toBe('border-t border-white/10')
|
||||
expect(cn('border-t border-white')).toBe('border-t border-white')
|
||||
expect(cn('text-3.5xl text-black')).toBe('text-3.5xl text-black')
|
||||
})
|
||||
|
||||
test('classnames combined with tailwind-merge', () => {
|
||||
expect(cn('text-right', {
|
||||
'text-center': true,
|
||||
})).toBe('text-center')
|
||||
|
||||
expect(cn('text-right', {
|
||||
'text-center': false,
|
||||
})).toBe('text-right')
|
||||
})
|
||||
})
|
2110
web/yarn.lock
2110
web/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user