| { | |||||
| "recommendations": [ | |||||
| "bradlc.vscode-tailwindcss", | |||||
| "firsttris.vscode-jest-runner" | |||||
| ] | |||||
| } |
| If your IDE is VSCode, rename `web/.vscode/settings.example.json` to `web/.vscode/settings.json` for lint code setting. | 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 | ## Documentation | ||||
| Visit <https://docs.dify.ai/getting-started/readme> to view the full documentation. | Visit <https://docs.dify.ai/getting-started/readme> to view the full documentation. |
| 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() | |||||
| }) | |||||
| }) |
| {...props} | {...props} | ||||
| > | > | ||||
| {children} | {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> | </button> | ||||
| ) | ) | ||||
| }, | }, |
| /** | |||||
| * 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) |
| "prepare": "cd ../ && node -e \"if (process.env.NODE_ENV !== 'production'){process.exit(1)} \" || husky install ./web/.husky", | "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", | "gen-icons": "node ./app/components/base/icons/script.js", | ||||
| "uglify-embed": "node ./bin/uglify-embed", | "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": { | "dependencies": { | ||||
| "@babel/runtime": "^7.22.3", | "@babel/runtime": "^7.22.3", | ||||
| "@antfu/eslint-config": "^0.36.0", | "@antfu/eslint-config": "^0.36.0", | ||||
| "@faker-js/faker": "^7.6.0", | "@faker-js/faker": "^7.6.0", | ||||
| "@rgrove/parse-xml": "^4.1.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/crypto-js": "^4.1.1", | ||||
| "@types/dagre": "^0.7.52", | "@types/dagre": "^0.7.52", | ||||
| "@types/jest": "^29.5.12", | |||||
| "@types/js-cookie": "^3.0.3", | "@types/js-cookie": "^3.0.3", | ||||
| "@types/lodash-es": "^4.17.7", | "@types/lodash-es": "^4.17.7", | ||||
| "@types/negotiator": "^0.6.1", | "@types/negotiator": "^0.6.1", | ||||
| "eslint": "^8.36.0", | "eslint": "^8.36.0", | ||||
| "eslint-config-next": "^14.0.4", | "eslint-config-next": "^14.0.4", | ||||
| "husky": "^8.0.3", | "husky": "^8.0.3", | ||||
| "jest": "^29.7.0", | |||||
| "jest-environment-jsdom": "^29.7.0", | |||||
| "lint-staged": "^13.2.2", | "lint-staged": "^13.2.2", | ||||
| "postcss": "^8.4.31", | "postcss": "^8.4.31", | ||||
| "sass": "^1.61.0", | "sass": "^1.61.0", | ||||
| "tailwindcss": "^3.4.4", | "tailwindcss": "^3.4.4", | ||||
| "ts-node": "^10.9.2", | |||||
| "typescript": "4.9.5", | "typescript": "4.9.5", | ||||
| "uglify-js": "^3.17.4" | "uglify-js": "^3.17.4" | ||||
| }, | }, |
| 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') | |||||
| }) | |||||
| }) |