feat: support e2e (#2624)

* chore: upgrade vitest to v0.34.3

* feat: setup NocoBase

* chore: preparing test env

* test: add a test of rigster

* refactor: rename test dir to testUtils

* chore: add tests

* chore: add ci for e2e

* chore: fix ci

* chore: avoid error in CI

* chore: add some utils for test

* chore: make more stable

* chore: should not close server in CI

* chore: add comments

* chore: change output dir

* fix: should use current branch to run tests

* chore: should request systemSettings by api in e2e

* chore: should build first in e2e CI

* chore: remove key

* chore: use execa to replace execSync

* refactor: extract test suite

* chore: add gotoPage

* chore: update uid of pageSchema

* chore: update collection name

* chore: use faker.js to generate data

* refactor: extract page config

* chore: ignore for association fields in faker

* chore: add testid

* chore: optimize action designer

* chore: associationFilter.Item designer

* chore: AssiciationFilter & BlockItem

* Revert "chore: AssiciationFilter & BlockItem"

This reverts commit b418df650e106fd0c8e23035d2f75acf60dcafe4.

* Revert "chore: associationFilter.Item designer"

This reverts commit 7aa4d35c1af7f3a780b370d8b1b44aac01697be3.

* Revert "chore: optimize action designer"

This reverts commit ff717b972ffd64f7968d565a3a84ad617ff889e2.

* chore: optimize Designer

* chore: compat with older browsers

* chore: use describe to avoid hooks is not run

* chore: add no-floating-promises to eslint rules

* chore: support argv

* chore: demo

* chore: better testId

* chore: change .e2e.ts to .test.ts

* fix(SchemaInitializer): avoid error

* refactor: move e2eUtils.ts to @nocobase/test

* fix: move e2eUtils to client

* chore: remove uselesscode

* refactor: add .env.e2e.example

* chore: optimize log

* refactor: use mockPage to replace gotoPage

* chore: update env.e2e

* chore: add APP_BASE_URL

* chore: gitigore

* test: add test related of menu

* chore: add SOCKET_PATH in env

* fix(vscode): load env when using vscode plugin
This commit is contained in:
被雨水过滤的空气-Rain 2023-09-27 20:00:17 +08:00 committed by GitHub
parent ce879d2dda
commit a57c93d35b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
143 changed files with 2355 additions and 163 deletions

97
.env.e2e.example Normal file
View File

@ -0,0 +1,97 @@
################# DOCKER #################
ADMINER_PORT=10101
DB_MYSQL_PORT=10102
DB_POSTGRES_PORT=10103
VERDACCIO_PORT=10104
# VERDACCIO_URL=http://host.docker.internal:10104/
################# NOCOBASE APPLICATION #################
# !!! When `APP_ENV=production`, opening http://localhost:13000/ will show "Not Found".
# !!! It is recommended to use nginx to proxy static files. For example https://github.com/nocobase/nocobase/blob/main/docker/nocobase/nocobase.conf
APP_ENV=development
APP_PORT=20000
APP_BASE_URL=http://localhost:20000
APP_KEY=test-key-e2e
SOCKET_PATH=storage/gateway-e2e.sock
API_BASE_PATH=/api/
API_BASE_URL=
PROXY_TARGET_URL=
LOGGER_TRANSPORT=
LOGGER_LEVEL=
LOGGER_BASE_PATH=storage/logs-e2e
################# DATABASE #################
DB_DIALECT=sqlite
DB_STORAGE=storage/db/nocobase-e2e.sqlite
DB_TABLE_PREFIX=
# DB_HOST=localhost
# DB_PORT=5432
# DB_DATABASE=nocobase-e2e
# DB_USER=nocobase
# DB_PASSWORD=nocobase
# DB_LOGGING=on
# DB_UNDERSCORED=false
#== SSL CONFIG ==#
# DB_DIALECT_OPTIONS_SSL_CA=
# DB_DIALECT_OPTIONS_SSL_KEY=
# DB_DIALECT_OPTIONS_SSL_CERT=
# DB_DIALECT_OPTIONS_SSL_REJECT_UNAUTHORIZED=true
################# CACHE #################
# default is memory cache, when develop mode,code's change will be clear memory cache, so can use 'cache-manager-fs-hash'
# CACHE_CONFIG={"storePackage":"cache-manager-fs-hash","ttl":86400,"max":1000}
################# STORAGE (Initialization only) #################
INIT_ROOT_EMAIL=admin@nocobase.com
INIT_ROOT_PASSWORD=admin123
INIT_ROOT_NICKNAME=Super Admin
INIT_ROOT_USERNAME=nocobase
# local or ali-oss
DEFAULT_STORAGE_TYPE=local
# LOCAL STORAGE
LOCAL_STORAGE_BASE_URL=/storage/uploads-e2e
LOCAL_STORAGE_DEST=storage/uploads-e2e
# ALI OSS STORAGE
ALI_OSS_STORAGE_BASE_URL=
ALI_OSS_REGION=oss-cn-beijing
ALI_OSS_ACCESS_KEY_ID=
ALI_OSS_ACCESS_KEY_SECRET=
ALI_OSS_BUCKET=
# Tencent COS STORAGE
TX_COS_STORAGE_BASE_URL=
TX_COS_REGION=ap-guangzhou
TX_COS_SECRET_ID=
TX_COS_SECRET_KEY=
TX_COS_BUCKET=
# AWS
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_S3_REGION=
AWS_S3_BUCKET=
AWS_S3_STORAGE_BASE_URL=
# ALI SMS VERIFY CODE CONFIG
INIT_ALI_SMS_ACCESS_KEY=
INIT_ALI_SMS_ACCESS_KEY_SECRET=
INIT_ALI_SMS_ENDPOINT=
INIT_ALI_SMS_VERIFY_CODE_TEMPLATE=
INIT_ALI_SMS_VERIFY_CODE_SIGN=
# use any string name (no space)
DEFAULT_SMS_VERIFY_CODE_PROVIDER=
# in nodejs 17+ that SSL v3 causes some ecosystem libraries to become incompatible. Configuring this option can prevent upgrading SSL V3
# NODE_OPTIONS=--openssl-legacy-provider

View File

@ -38,7 +38,8 @@
"sourceType": "module", "sourceType": "module",
"ecmaFeatures": { "ecmaFeatures": {
"jsx": true "jsx": true
} },
"project": "./tsconfig.json"
}, },
"rules": { "rules": {
"@typescript-eslint/no-this-alias": "off", "@typescript-eslint/no-this-alias": "off",
@ -54,6 +55,7 @@
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-var-requires": "off", "@typescript-eslint/no-var-requires": "off",
"promise/always-return": "off" "promise/always-return": "off",
"@typescript-eslint/no-floating-promises": "error"
} }
} }

36
.github/workflows/nocobase-test-e2e.yml vendored Normal file
View File

@ -0,0 +1,36 @@
name: NocoBase E2E Test
on:
push:
branches:
- 'main'
- 'develop'
paths:
- '.github/workflows/nocobase-build-test.yml'
- 'packages/**'
pull_request:
branches:
- '**'
paths:
- '.github/workflows/nocobase-build-test.yml'
- 'packages/**'
jobs:
e2e-test:
strategy:
matrix:
node_version: ['18']
runs-on: ubuntu-latest
container: node:${{ matrix.node_version }}
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node_version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node_version }}
cache: 'yarn'
- run: yarn install
- run: yarn build
- run: npx playwright install --with-deps
- run: yarn test:e2e
timeout-minutes: 30

4
.gitignore vendored
View File

@ -3,6 +3,7 @@ lib/
esm/ esm/
es/ es/
.env .env
.env.e2e
.DS_Store .DS_Store
yarn-error.log yarn-error.log
lerna-debug.log lerna-debug.log
@ -30,4 +31,7 @@ storage/plugins
storage/tar storage/tar
storage/tmp storage/tmp
storage/app.watch.ts storage/app.watch.ts
storage/logs-e2e
storage/uploads-e2e
tsconfig.paths.json tsconfig.paths.json
playwright

13
.vscode/launch.json vendored
View File

@ -37,6 +37,19 @@
"runtimeArgs": ["run", "test:client", "${relativeFile}"] "runtimeArgs": ["run", "test:client", "${relativeFile}"]
} }
}, },
{
"type": "node",
"request": "launch",
"name": "Debug E2E Tests",
"runtimeExecutable": "yarn",
"runtimeArgs": ["test:e2e", "${file}"],
"skipFiles": ["<node_internals>/**"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"windows": {
"runtimeArgs": ["test:e2e", "${fileBasename}"]
}
},
{ {
"type": "node", "type": "node",
"request": "launch", "request": "launch",

View File

@ -25,6 +25,9 @@
"tar": "nocobase tar", "tar": "nocobase tar",
"test": "nocobase test", "test": "nocobase test",
"test:client": "vitest", "test:client": "vitest",
"test:e2e": "ts-node-dev ./scripts/runE2e.setup.ts",
"test:codegen": "ts-node-dev ./scripts/codegen.setup.ts",
"test:server": "ts-node-dev ./scripts/nocobase.setup.ts",
"tc": "yarn test:client", "tc": "yarn test:client",
"doc": "nocobase doc", "doc": "nocobase doc",
"postinstall": "nocobase postinstall", "postinstall": "nocobase postinstall",
@ -64,6 +67,8 @@
"@commitlint/cli": "^16.1.0", "@commitlint/cli": "^16.1.0",
"@commitlint/config-conventional": "^16.0.0", "@commitlint/config-conventional": "^16.0.0",
"@commitlint/prompt-cli": "^16.1.0", "@commitlint/prompt-cli": "^16.1.0",
"@faker-js/faker": "8.1.0",
"@playwright/test": "^1.37.1",
"@testing-library/jest-dom": "^5.17.0", "@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^14.0.0", "@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3", "@testing-library/user-event": "^14.4.3",
@ -76,6 +81,7 @@
"dumi-theme-nocobase": "^0.2.14", "dumi-theme-nocobase": "^0.2.14",
"eslint-plugin-jest-dom": "^5.0.1", "eslint-plugin-jest-dom": "^5.0.1",
"eslint-plugin-testing-library": "^5.11.0", "eslint-plugin-testing-library": "^5.11.0",
"execa": "^5.1.1",
"ghooks": "^2.0.4", "ghooks": "^2.0.4",
"jest": "^29.6.2", "jest": "^29.6.2",
"jest-cli": "^29.6.2", "jest-cli": "^29.6.2",

View File

@ -0,0 +1,256 @@
import { expect, test } from '@nocobase/test/client';
const pageConfig = {
collections: [
{
name: 't_wbjenhqk5dz',
title: 'collection1',
inherit: false,
hidden: false,
description: null,
fields: [
{
name: 'id',
type: 'bigInt',
interface: 'id',
description: null,
collectionName: 't_wbjenhqk5dz',
parentKey: null,
reverseKey: null,
autoIncrement: true,
primaryKey: true,
allowNull: false,
uiSchema: {
type: 'number',
title: '{{t("ID")}}',
'x-component': 'InputNumber',
'x-read-pretty': true,
},
},
{
name: 'createdAt',
type: 'date',
interface: 'createdAt',
description: null,
collectionName: 't_wbjenhqk5dz',
parentKey: null,
reverseKey: null,
field: 'createdAt',
uiSchema: {
type: 'datetime',
title: '{{t("Created at")}}',
'x-component': 'DatePicker',
'x-component-props': {},
'x-read-pretty': true,
},
},
{
name: 'createdBy',
type: 'belongsTo',
interface: 'createdBy',
description: null,
collectionName: 't_wbjenhqk5dz',
parentKey: null,
reverseKey: null,
target: 'users',
foreignKey: 'createdById',
uiSchema: {
type: 'object',
title: '{{t("Created by")}}',
'x-component': 'AssociationField',
'x-component-props': {
fieldNames: {
value: 'id',
label: 'nickname',
},
},
'x-read-pretty': true,
},
targetKey: 'id',
},
{
name: 'updatedAt',
type: 'date',
interface: 'updatedAt',
description: null,
collectionName: 't_wbjenhqk5dz',
parentKey: null,
reverseKey: null,
field: 'updatedAt',
uiSchema: {
type: 'string',
title: '{{t("Last updated at")}}',
'x-component': 'DatePicker',
'x-component-props': {},
'x-read-pretty': true,
},
},
{
name: 'updatedBy',
type: 'belongsTo',
interface: 'updatedBy',
description: null,
collectionName: 't_wbjenhqk5dz',
parentKey: null,
reverseKey: null,
target: 'users',
foreignKey: 'updatedById',
uiSchema: {
type: 'object',
title: '{{t("Last updated by")}}',
'x-component': 'AssociationField',
'x-component-props': {
fieldNames: {
value: 'id',
label: 'nickname',
},
},
'x-read-pretty': true,
},
targetKey: 'id',
},
],
category: [],
logging: true,
autoGenId: true,
createdBy: true,
updatedBy: true,
createdAt: true,
updatedAt: true,
sortable: true,
template: 'general',
view: false,
},
{
name: 't_u0c1mmzldgo',
title: 'collection2',
inherit: false,
hidden: false,
description: null,
fields: [
{
name: 'id',
type: 'bigInt',
interface: 'id',
description: null,
collectionName: 't_u0c1mmzldgo',
parentKey: null,
reverseKey: null,
autoIncrement: true,
primaryKey: true,
allowNull: false,
uiSchema: {
type: 'number',
title: '{{t("ID")}}',
'x-component': 'InputNumber',
'x-read-pretty': true,
},
},
{
name: 'createdAt',
type: 'date',
interface: 'createdAt',
description: null,
collectionName: 't_u0c1mmzldgo',
parentKey: null,
reverseKey: null,
field: 'createdAt',
uiSchema: {
type: 'datetime',
title: '{{t("Created at")}}',
'x-component': 'DatePicker',
'x-component-props': {},
'x-read-pretty': true,
},
},
{
name: 'createdBy',
type: 'belongsTo',
interface: 'createdBy',
description: null,
collectionName: 't_u0c1mmzldgo',
parentKey: null,
reverseKey: null,
target: 'users',
foreignKey: 'createdById',
uiSchema: {
type: 'object',
title: '{{t("Created by")}}',
'x-component': 'AssociationField',
'x-component-props': {
fieldNames: {
value: 'id',
label: 'nickname',
},
},
'x-read-pretty': true,
},
targetKey: 'id',
},
{
name: 'updatedAt',
type: 'date',
interface: 'updatedAt',
description: null,
collectionName: 't_u0c1mmzldgo',
parentKey: null,
reverseKey: null,
field: 'updatedAt',
uiSchema: {
type: 'string',
title: '{{t("Last updated at")}}',
'x-component': 'DatePicker',
'x-component-props': {},
'x-read-pretty': true,
},
},
{
name: 'updatedBy',
type: 'belongsTo',
interface: 'updatedBy',
description: null,
collectionName: 't_u0c1mmzldgo',
parentKey: null,
reverseKey: null,
target: 'users',
foreignKey: 'updatedById',
uiSchema: {
type: 'object',
title: '{{t("Last updated by")}}',
'x-component': 'AssociationField',
'x-component-props': {
fieldNames: {
value: 'id',
label: 'nickname',
},
},
'x-read-pretty': true,
},
targetKey: 'id',
},
],
category: [],
logging: true,
autoGenId: true,
createdBy: true,
updatedBy: true,
createdAt: true,
updatedAt: true,
sortable: true,
template: 'general',
view: false,
},
],
};
test.describe('createCollections', () => {
test('quickly create collections', async ({ page, mockPage }) => {
await mockPage(pageConfig).goto();
await page.getByRole('button', { name: 'plus Add block' }).hover();
await page.getByRole('menuitem', { name: 'table Table right' }).hover();
await expect(page.getByRole('menuitem', { name: 'collection1' })).toBeVisible();
await expect(page.getByRole('menuitem', { name: 'collection2' })).toBeVisible();
});
});

View File

@ -0,0 +1,53 @@
import { expect, test } from '@nocobase/test/client';
test.describe('create data block', () => {
test('table', async ({ page, mockPage }) => {
await mockPage().goto();
await page.getByRole('button', { name: 'plus Add block' }).hover();
await page.getByRole('menuitem', { name: 'table Table right' }).hover();
await page.getByRole('menuitem', { name: 'Users' }).click();
await expect(page.getByRole('columnheader', { name: 'setting Configure columns' })).toBeVisible();
});
test('form', async ({ page, mockPage }) => {
await mockPage().goto();
await page.getByRole('button', { name: 'plus Add block' }).hover();
await page.getByRole('menuitem', { name: 'form Form right' }).first().hover();
await page.getByRole('menuitem', { name: 'Users' }).click();
await expect(page.getByRole('button', { name: 'setting Configure fields' })).toBeVisible();
});
test('details', async ({ page, mockPage }) => {
await mockPage().goto();
await page.getByRole('button', { name: 'plus Add block' }).hover();
await page.getByRole('menuitem', { name: 'table Details right' }).hover();
await page.getByRole('menuitem', { name: 'Users' }).click();
await expect(page.getByRole('button', { name: 'setting Configure fields' })).toBeVisible();
});
test('list', async ({ page, mockPage }) => {
await mockPage().goto();
await page.getByRole('button', { name: 'plus Add block' }).hover();
await page.getByRole('menuitem', { name: 'ordered-list List right' }).hover();
await page.getByRole('menuitem', { name: 'Users' }).click();
await expect(page.getByRole('button', { name: 'setting Configure fields' }).first()).toBeVisible();
});
test('grid card', async ({ page, mockPage }) => {
await mockPage().goto();
await page.getByRole('button', { name: 'plus Add block' }).hover();
await page.getByRole('menuitem', { name: 'ordered-list Grid Card right' }).hover();
await page.getByRole('menuitem', { name: 'Users' }).click();
await expect(page.getByRole('button', { name: 'setting Configure fields' }).first()).toBeVisible();
});
});

View File

@ -0,0 +1,324 @@
import { expect, test } from '@nocobase/test/client';
const phonePageConfig = {
collections: [
{
key: '94kecytzenp',
name: 't_x3mxc1ymorw',
title: 'faker-testing',
inherit: false,
hidden: false,
description: null,
fields: [
{
key: 'cgzlv8nu6fr',
name: 'id',
type: 'bigInt',
interface: 'id',
description: null,
collectionName: 't_x3mxc1ymorw',
parentKey: null,
reverseKey: null,
autoIncrement: true,
primaryKey: true,
allowNull: false,
uiSchema: {
type: 'number',
title: '{{t("ID")}}',
'x-component': 'InputNumber',
'x-read-pretty': true,
},
},
{
key: 'sd7xf79138a',
name: 'createdAt',
type: 'date',
interface: 'createdAt',
description: null,
collectionName: 't_x3mxc1ymorw',
parentKey: null,
reverseKey: null,
field: 'createdAt',
uiSchema: {
type: 'datetime',
title: '{{t("Created at")}}',
'x-component': 'DatePicker',
'x-component-props': {},
'x-read-pretty': true,
},
},
{
key: 'z063hvkfdtf',
name: 'createdBy',
type: 'belongsTo',
interface: 'createdBy',
description: null,
collectionName: 't_x3mxc1ymorw',
parentKey: null,
reverseKey: null,
target: 'users',
foreignKey: 'createdById',
uiSchema: {
type: 'object',
title: '{{t("Created by")}}',
'x-component': 'AssociationField',
'x-component-props': {
fieldNames: {
value: 'id',
label: 'nickname',
},
},
'x-read-pretty': true,
},
targetKey: 'id',
},
{
key: 'wlnvpjkuv3i',
name: 'updatedAt',
type: 'date',
interface: 'updatedAt',
description: null,
collectionName: 't_x3mxc1ymorw',
parentKey: null,
reverseKey: null,
field: 'updatedAt',
uiSchema: {
type: 'string',
title: '{{t("Last updated at")}}',
'x-component': 'DatePicker',
'x-component-props': {},
'x-read-pretty': true,
},
},
{
key: 'rxyq48pu0kd',
name: 'updatedBy',
type: 'belongsTo',
interface: 'updatedBy',
description: null,
collectionName: 't_x3mxc1ymorw',
parentKey: null,
reverseKey: null,
target: 'users',
foreignKey: 'updatedById',
uiSchema: {
type: 'object',
title: '{{t("Last updated by")}}',
'x-component': 'AssociationField',
'x-component-props': {
fieldNames: {
value: 'id',
label: 'nickname',
},
},
'x-read-pretty': true,
},
targetKey: 'id',
},
{
key: '3b24xiumcck',
name: 'f_fyjoexeqvuh',
type: 'string',
interface: 'phone',
description: null,
collectionName: 't_x3mxc1ymorw',
parentKey: null,
reverseKey: null,
uiSchema: {
type: 'string',
'x-component': 'Input',
'x-component-props': {
type: 'tel',
},
title: 'Phone',
},
},
],
category: [],
logging: true,
autoGenId: true,
createdBy: true,
updatedBy: true,
createdAt: true,
updatedAt: true,
sortable: true,
template: 'general',
view: false,
},
],
pageSchema: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Page',
properties: {
zyfy6q68u10: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid',
'x-initializer': 'BlockInitializers',
properties: {
sfe29sssqks: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
properties: {
wr0q46863ri: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
properties: {
'996h7puslon': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-decorator': 'TableBlockProvider',
'x-acl-action': 't_x3mxc1ymorw:list',
'x-decorator-props': {
collection: 't_x3mxc1ymorw',
resource: 't_x3mxc1ymorw',
action: 'list',
params: {
pageSize: 20,
},
rowKey: 'id',
showIndex: true,
dragSort: false,
disableTemplate: false,
},
'x-designer': 'TableBlockDesigner',
'x-component': 'CardItem',
'x-filter-targets': [],
properties: {
actions: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-initializer': 'TableActionInitializers',
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 'var(--nb-spacing)',
},
},
'x-uid': 'to6wvr2ymud',
'x-async': false,
'x-index': 1,
},
fmioqg3ac22: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'array',
'x-initializer': 'TableColumnInitializers',
'x-component': 'TableV2',
'x-component-props': {
rowKey: 'id',
rowSelection: {
type: 'checkbox',
},
useProps: '{{ useTableBlockProps }}',
},
properties: {
actions: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{ t("Actions") }}',
'x-action-column': 'actions',
'x-decorator': 'TableV2.Column.ActionBar',
'x-component': 'TableV2.Column',
'x-designer': 'TableV2.ActionColumnDesigner',
'x-initializer': 'TableActionColumnInitializers',
properties: {
actions: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-decorator': 'DndContext',
'x-component': 'Space',
'x-component-props': {
split: '|',
},
'x-uid': '7ueb2r7aiq2',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'q6tqbavh1hz',
'x-async': false,
'x-index': 1,
},
'7x5qve01k29': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-decorator': 'TableV2.Column.Decorator',
'x-designer': 'TableV2.Column.Designer',
'x-component': 'TableV2.Column',
properties: {
f_fyjoexeqvuh: {
_isJSONSchemaObject: true,
version: '2.0',
'x-collection-field': 't_x3mxc1ymorw.f_fyjoexeqvuh',
'x-component': 'CollectionField',
'x-component-props': {},
'x-read-pretty': true,
'x-decorator': null,
'x-decorator-props': {
labelStyle: {
display: 'none',
},
},
'x-uid': 'bmewjcb9996',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '6vqo25ezxbr',
'x-async': false,
'x-index': 2,
},
},
'x-uid': 'esirxkr0lca',
'x-async': false,
'x-index': 2,
},
},
'x-uid': 'zcbhgqtrof5',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'tsma8ix1lun',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'q8bpsoqjz1b',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'edhta7p0qtf',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '1o0p25d72br',
'x-async': true,
'x-index': 1,
},
};
test.describe('faker', () => {
test('phone', async ({ page, mockPage }) => {
await mockPage(phonePageConfig).goto();
await expect(page.getByRole('cell', { name: '14979013912' })).toBeVisible();
await expect(page.getByRole('cell', { name: '10313363958' })).toBeVisible();
await expect(page.getByRole('cell', { name: '14365248205' })).toBeVisible();
});
});

View File

@ -0,0 +1,23 @@
import { enableToConfig, expect, test } from '@nocobase/test/client';
test.describe('menu', () => {
test('create new page, then delete', async ({ page }) => {
await page.goto('/');
await enableToConfig(page);
await page.getByTestId('add-menu-item-button-in-header').hover();
await page.getByRole('menuitem', { name: 'Page' }).click();
await page.getByRole('textbox').click();
await page.getByRole('textbox').fill('new page');
await page.getByRole('button', { name: 'OK' }).click();
await page.getByText('new page').click();
await expect(page.getByTestId('add-block-button-in-page')).toBeVisible();
// 删除页面,避免影响其他测试
await page.getByRole('menu').getByText('new page').hover();
await page.getByTestId('designer-schema-settings').hover();
await page.getByRole('menuitem', { name: 'Delete' }).click();
await page.getByRole('button', { name: 'OK' }).click();
});
});

View File

@ -0,0 +1,462 @@
import { expect, test } from '@nocobase/test/client';
const pageConfig = {
pageSchema: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Page',
properties: {
pdxwiogf3pc: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid',
'x-initializer': 'BlockInitializers',
properties: {
txjj2q9859s: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
properties: {
w4ziz4txnrn: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
properties: {
xpfe7mh0n0p: {
'x-uid': '7pdacls95qk',
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-decorator': 'TableBlockProvider',
'x-acl-action': 'users:list',
'x-decorator-props': {
collection: 'users',
resource: 'users',
action: 'list',
params: {
pageSize: 20,
},
rowKey: 'id',
showIndex: true,
dragSort: false,
disableTemplate: false,
},
'x-designer': 'TableBlockDesigner',
'x-component': 'CardItem',
'x-filter-targets': [],
'x-component-props': {
title: 'Table block',
},
properties: {
actions: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-initializer': 'TableActionInitializers',
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 'var(--nb-spacing)',
},
},
'x-uid': 'wb5mvwm537t',
'x-async': false,
'x-index': 1,
},
xj9bu0sv4yz: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'array',
'x-initializer': 'TableColumnInitializers',
'x-component': 'TableV2',
'x-component-props': {
rowKey: 'id',
rowSelection: {
type: 'checkbox',
},
useProps: '{{ useTableBlockProps }}',
},
properties: {
actions: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{ t("Actions") }}',
'x-action-column': 'actions',
'x-decorator': 'TableV2.Column.ActionBar',
'x-component': 'TableV2.Column',
'x-designer': 'TableV2.ActionColumnDesigner',
'x-initializer': 'TableActionColumnInitializers',
properties: {
actions: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-decorator': 'DndContext',
'x-component': 'Space',
'x-component-props': {
split: '|',
},
'x-uid': '33rzky3kemq',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '7nnw5grzet8',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'm54bsjcv09a',
'x-async': false,
'x-index': 2,
},
},
'x-async': false,
'x-index': 1,
},
},
'x-uid': '4yi4kr1wgkz',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'g6bhfeaba55',
'x-async': false,
'x-index': 1,
},
j9xgdusioku: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
properties: {
'4pdar7yt1d1': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
properties: {
prtqni04a5t: {
'x-uid': 'coyws8els85',
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-acl-action-props': {
skipScopeCheck: true,
},
'x-acl-action': 'users:create',
'x-decorator': 'FormBlockProvider',
'x-decorator-props': {
resource: 'users',
collection: 'users',
},
'x-designer': 'FormV2.Designer',
'x-component': 'CardItem',
'x-component-props': {
title: 'Form block',
},
properties: {
uo3q3bs1l4c: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'FormV2',
'x-component-props': {
useProps: '{{ useFormBlockProps }}',
},
properties: {
grid: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid',
'x-initializer': 'FormItemInitializers',
'x-uid': '54w1s4gkrp4',
'x-async': false,
'x-index': 1,
},
actions: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-initializer': 'FormActionInitializers',
'x-component': 'ActionBar',
'x-component-props': {
layout: 'one-column',
style: {
marginTop: 24,
},
},
'x-uid': 'wx1ymybhw4d',
'x-async': false,
'x-index': 2,
},
},
'x-uid': '47x9aotslaf',
'x-async': false,
'x-index': 1,
},
},
'x-async': false,
'x-index': 1,
},
},
'x-uid': '4tdyzz3hh3s',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '5pr5x41th9y',
'x-async': false,
'x-index': 2,
},
hlm6jtcx5px: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
properties: {
'0if9syydrxn': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
properties: {
fhi0k7tswj1: {
'x-uid': 'dntzs4ojq9y',
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-acl-action': 'users:view',
'x-decorator': 'DetailsBlockProvider',
'x-decorator-props': {
resource: 'users',
collection: 'users',
readPretty: true,
action: 'list',
params: {
pageSize: 1,
},
rowKey: 'id',
},
'x-designer': 'DetailsDesigner',
'x-component': 'CardItem',
'x-component-props': {
title: 'Details block',
},
properties: {
ru4sjmi898d: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Details',
'x-read-pretty': true,
'x-component-props': {
useProps: '{{ useDetailsBlockProps }}',
},
properties: {
'74jvk36v996': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-initializer': 'DetailsActionInitializers',
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 24,
},
},
'x-uid': 'zpt2ewuvd3i',
'x-async': false,
'x-index': 1,
},
grid: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid',
'x-initializer': 'ReadPrettyFormItemInitializers',
'x-uid': 'ivgyowpi1rj',
'x-async': false,
'x-index': 2,
},
pagination: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Pagination',
'x-component-props': {
useProps: '{{ useDetailsPaginationProps }}',
},
'x-uid': 'yl3bc57sqmx',
'x-async': false,
'x-index': 3,
},
},
'x-uid': 't3sshcjbw53',
'x-async': false,
'x-index': 1,
},
},
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'so9f6fq7nmf',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'jdrrxpdel86',
'x-async': false,
'x-index': 3,
},
ipq256mnxif: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
properties: {
h65018f1z8b: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
properties: {
ym27n2ko5vr: {
'x-uid': 'j45qq035eas',
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-acl-action': 'users:view',
'x-decorator': 'List.Decorator',
'x-decorator-props': {
resource: 'users',
collection: 'users',
readPretty: true,
action: 'list',
params: {
pageSize: 10,
},
runWhenParamsChanged: true,
rowKey: 'id',
},
'x-component': 'CardItem',
'x-designer': 'List.Designer',
'x-component-props': {
title: 'List block',
},
properties: {
actionBar: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-initializer': 'ListActionInitializers',
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 'var(--nb-spacing)',
},
},
'x-uid': 'c66vjxpf8bf',
'x-async': false,
'x-index': 1,
},
list: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'array',
'x-component': 'List',
'x-component-props': {
props: '{{ useListBlockProps }}',
},
properties: {
item: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'object',
'x-component': 'List.Item',
'x-read-pretty': true,
'x-component-props': {
useProps: '{{ useListItemProps }}',
},
properties: {
grid: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid',
'x-initializer': 'ReadPrettyFormItemInitializers',
'x-initializer-props': {
useProps: '{{ useListItemInitializerProps }}',
},
'x-uid': 'uqml2dwp9rv',
'x-async': false,
'x-index': 1,
},
actionBar: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-align': 'left',
'x-initializer': 'ListItemActionInitializers',
'x-component': 'ActionBar',
'x-component-props': {
useProps: '{{ useListActionBarProps }}',
layout: 'one-column',
},
'x-uid': 'de6lsblu8nf',
'x-async': false,
'x-index': 2,
},
},
'x-uid': 'uzulduo8g1z',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 's53c3p9hmf1',
'x-async': false,
'x-index': 2,
},
},
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'zg9l4uw1rmy',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'qw9hle324xg',
'x-async': false,
'x-index': 4,
},
},
'x-uid': 'lvyncy91yyv',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'ny9ds6mjkkj',
'x-async': true,
'x-index': 1,
},
};
test.describe('pageSchema', () => {
test('quickly create page schema', async ({ page, mockPage }) => {
await mockPage(pageConfig).goto();
await expect(page.getByText('Table block')).toBeVisible();
await expect(page.getByText('Form block')).toBeVisible();
await expect(page.getByText('Details block')).toBeVisible();
await expect(page.getByText('List block')).toBeVisible();
});
});

View File

@ -16,7 +16,7 @@ const schema2: ISchema = {
export const ACLPane = () => { export const ACLPane = () => {
return ( return (
<Card bordered={false}> <Card data-testid="acl-pane-card" bordered={false}>
<SchemaComponent components={components} schema={schema2} /> <SchemaComponent components={components} schema={schema2} />
</Card> </Card>
); );

View File

@ -84,6 +84,7 @@ export const StrategyActions = connect((props) => {
render: (scope, action) => render: (scope, action) =>
!action.onNewRecord && ( !action.onNewRecord && (
<Select <Select
data-testid="antd-select"
popupMatchSelectWidth={false} popupMatchSelectWidth={false}
size={'small'} size={'small'}
value={scope} value={scope}

View File

@ -289,7 +289,7 @@ export const BlockProvider = (props) => {
<BlockRequestProvider {...props} updateAssociationValues={updateAssociationValues} params={params}> <BlockRequestProvider {...props} updateAssociationValues={updateAssociationValues} params={params}>
<SharedFilterProvider {...props} params={params}> <SharedFilterProvider {...props} params={params}>
<FilterBlockRecord {...props} params={params}> <FilterBlockRecord {...props} params={params}>
{props.children} <div data-testid={props['data-testid']}>{props.children}</div>
</FilterBlockRecord> </FilterBlockRecord>
</SharedFilterProvider> </SharedFilterProvider>
</BlockRequestProvider> </BlockRequestProvider>

View File

@ -33,7 +33,7 @@ const InternalCalendarBlockProvider = (props) => {
export const CalendarBlockProvider = (props) => { export const CalendarBlockProvider = (props) => {
return ( return (
<BlockProvider {...props} params={{ ...props.params, paginate: false }}> <BlockProvider data-testid="calendar-block" {...props} params={{ ...props.params, paginate: false }}>
<InternalCalendarBlockProvider {...props} /> <InternalCalendarBlockProvider {...props} />
</BlockProvider> </BlockProvider>
); );

View File

@ -39,7 +39,7 @@ const InternalDetailsBlockProvider = (props) => {
export const DetailsBlockProvider = (props) => { export const DetailsBlockProvider = (props) => {
return ( return (
<BlockProvider {...props}> <BlockProvider data-testid="details-block" {...props}>
<InternalDetailsBlockProvider {...props} /> <InternalDetailsBlockProvider {...props} />
</BlockProvider> </BlockProvider>
); );

View File

@ -5,7 +5,7 @@ import { FormBlockProvider } from './FormBlockProvider';
export const FilterFormBlockProvider = (props) => { export const FilterFormBlockProvider = (props) => {
return ( return (
<DatePickerProvider value={{ utc: false }}> <DatePickerProvider value={{ utc: false }}>
<FormBlockProvider {...props}></FormBlockProvider> <FormBlockProvider data-testid="filter-form-block" {...props}></FormBlockProvider>
</DatePickerProvider> </DatePickerProvider>
); );
}; };

View File

@ -82,7 +82,7 @@ export const FormBlockProvider = (props) => {
(currentCollection.name === (collection?.name || collection) && !isEmptyRecord) || !currentCollection.name; (currentCollection.name === (collection?.name || collection) && !isEmptyRecord) || !currentCollection.name;
return ( return (
(detailFlag || createFlag || isCusomeizeCreate) && ( (detailFlag || createFlag || isCusomeizeCreate) && (
<BlockProvider {...props} block={'form'}> <BlockProvider data-testid={props['data-testid'] || 'form-block'} {...props} block={'form'}>
<InternalFormBlockProvider {...props} /> <InternalFormBlockProvider {...props} />
</BlockProvider> </BlockProvider>
) )

View File

@ -77,7 +77,7 @@ export const WithoutFormFieldResource = createContext(null);
export const FormFieldProvider = (props) => { export const FormFieldProvider = (props) => {
return ( return (
<WithoutFormFieldResource.Provider value={false}> <WithoutFormFieldResource.Provider value={false}>
<BlockProvider block={'FormField'} {...props}> <BlockProvider data-testid="form-field-block" block={'FormField'} {...props}>
<InternalFormFieldProvider {...props} /> <InternalFormFieldProvider {...props} />
</BlockProvider> </BlockProvider>
</WithoutFormFieldResource.Provider> </WithoutFormFieldResource.Provider>

View File

@ -73,7 +73,7 @@ const InternalGanttBlockProvider = (props) => {
export const GanttBlockProvider = (props) => { export const GanttBlockProvider = (props) => {
const params = { filter: props.params.filter, tree: true, paginate: false, sort: props.fieldNames.start }; const params = { filter: props.params.filter, tree: true, paginate: false, sort: props.fieldNames.start };
return ( return (
<BlockProvider {...props} params={params}> <BlockProvider data-testid="gantt-block" {...props} params={params}>
<TableBlockProvider {...props} params={params}> <TableBlockProvider {...props} params={params}>
<InternalGanttBlockProvider {...props} /> <InternalGanttBlockProvider {...props} />
</TableBlockProvider> </TableBlockProvider>

View File

@ -108,7 +108,7 @@ export const KanbanBlockProvider = (props) => {
params['appends'] = appends; params['appends'] = appends;
} }
return ( return (
<BlockProvider {...props} params={params}> <BlockProvider data-testid="kanban-block" {...props} params={params}>
<InternalKanbanBlockProvider {...props} params={params} /> <InternalKanbanBlockProvider {...props} params={params} />
</BlockProvider> </BlockProvider>
); );

View File

@ -97,7 +97,7 @@ export const TableBlockProvider = (props) => {
return ( return (
<SchemaComponentOptions scope={{ treeTable }}> <SchemaComponentOptions scope={{ treeTable }}>
<FormContext.Provider value={form}> <FormContext.Provider value={form}>
<BlockProvider {...props} params={params} runWhenParamsChanged> <BlockProvider data-testid="table-block" {...props} params={params} runWhenParamsChanged>
<InternalTableBlockProvider {...props} childrenColumnName={childrenColumnName} params={params} /> <InternalTableBlockProvider {...props} childrenColumnName={childrenColumnName} params={params} />
</BlockProvider> </BlockProvider>
</FormContext.Provider> </FormContext.Provider>

View File

@ -126,7 +126,7 @@ export const WithoutTableFieldResource = createContext(null);
export const TableFieldProvider = (props) => { export const TableFieldProvider = (props) => {
return ( return (
<WithoutTableFieldResource.Provider value={false}> <WithoutTableFieldResource.Provider value={false}>
<BlockProvider block={'TableField'} {...props}> <BlockProvider data-testid="table-field-block" block={'TableField'} {...props}>
<InternalTableFieldProvider {...props} /> <InternalTableFieldProvider {...props} />
</BlockProvider> </BlockProvider>
</WithoutTableFieldResource.Provider> </WithoutTableFieldResource.Provider>

View File

@ -238,7 +238,7 @@ export const TableSelectorProvider = (props: TableSelectorProviderProps) => {
return ( return (
<SchemaComponentOptions scope={{ treeTable }}> <SchemaComponentOptions scope={{ treeTable }}>
<BlockProvider {...props} params={params}> <BlockProvider data-testid="table-selector-block" {...props} params={params}>
<InternalTableSelectorProvider {...props} params={params} extraFilter={extraFilter} /> <InternalTableSelectorProvider {...props} params={params} extraFilter={extraFilter} />
</BlockProvider> </BlockProvider>
</SchemaComponentOptions> </SchemaComponentOptions>

View File

@ -17,6 +17,7 @@ export const SourceForeignKey = observer(
return ( return (
<div> <div>
<Select <Select
data-testid="antd-select"
allowClear allowClear
placeholder={'留空时,自动生成 FK 字段'} placeholder={'留空时,自动生成 FK 字段'}
disabled={field.disabled} disabled={field.disabled}
@ -44,6 +45,7 @@ export const ThroughForeignKey = observer(
return ( return (
<div> <div>
<Select <Select
data-testid="antd-select"
allowClear allowClear
popupMatchSelectWidth={false} popupMatchSelectWidth={false}
placeholder={'留空时,自动生成 FK 字段'} placeholder={'留空时,自动生成 FK 字段'}
@ -72,6 +74,7 @@ export const TargetForeignKey = observer(
return ( return (
<div> <div>
<Select <Select
data-testid="antd-select"
allowClear allowClear
popupMatchSelectWidth={false} popupMatchSelectWidth={false}
placeholder={'留空时,自动生成 FK 字段'} placeholder={'留空时,自动生成 FK 字段'}
@ -101,6 +104,7 @@ export const SourceCollection = observer(
return ( return (
<div> <div>
<Select <Select
data-testid="antd-select"
disabled disabled
popupMatchSelectWidth={false} popupMatchSelectWidth={false}
value={collection.name} value={collection.name}
@ -116,7 +120,7 @@ export const SourceKey = observer(
() => { () => {
return ( return (
<div> <div>
<Select disabled value={'id'} options={[{ value: 'id', label: 'ID' }]} /> <Select data-testid="antd-select" disabled value={'id'} options={[{ value: 'id', label: 'ID' }]} />
</div> </div>
); );
}, },
@ -127,7 +131,7 @@ export const TargetKey = observer(
() => { () => {
return ( return (
<div> <div>
<Select disabled value={'id'} options={[{ value: 'id', label: 'ID' }]} /> <Select data-testid="antd-select" disabled value={'id'} options={[{ value: 'id', label: 'ID' }]} />
</div> </div>
); );
}, },

View File

@ -148,6 +148,7 @@ const PreviewCom = (props) => {
<Tag>{text}</Tag> <Tag>{text}</Tag>
) : ( ) : (
<Select <Select
data-testid="antd-select"
defaultValue={text} defaultValue={text}
popupMatchSelectWidth={false} popupMatchSelectWidth={false}
style={{ width: '100%' }} style={{ width: '100%' }}
@ -173,6 +174,7 @@ const PreviewCom = (props) => {
text text
) : ( ) : (
<Select <Select
data-testid="antd-select"
defaultValue={text} defaultValue={text}
style={{ width: '100%' }} style={{ width: '100%' }}
popupMatchSelectWidth={false} popupMatchSelectWidth={false}

View File

@ -67,6 +67,7 @@ export const SettingsCenterDropdown = () => {
<ActionContextProvider value={{ visible, setVisible }}> <ActionContextProvider value={{ visible, setVisible }}>
<Dropdown placement="bottom" menu={menu}> <Dropdown placement="bottom" menu={menu}>
<Button <Button
data-testid="settings-center-button"
icon={<SettingOutlined />} icon={<SettingOutlined />}
// title={t('All plugin settings')} // title={t('All plugin settings')}
/> />

View File

@ -28,6 +28,7 @@ export const ActionDrawer: ComposedActionDrawer = observer(
return ( return (
<Drawer <Drawer
data-testid="action-drawer"
width={openSizeWidthMap.get(openSize)} width={openSizeWidthMap.get(openSize)}
title={field.title} title={field.title}
{...others} {...others}

View File

@ -26,6 +26,7 @@ export const ActionModal: ComposedActionDrawer<ModalProps> = observer(
}); });
return ( return (
<Modal <Modal
data-testid="action-modal"
width={actualWidth} width={actualWidth}
title={field.title} title={field.title}
{...(others as ModalProps)} {...(others as ModalProps)}

View File

@ -28,7 +28,7 @@ export const ActionPage: ComposedActionDrawer = observer(
{containerRef?.current && {containerRef?.current &&
visible && visible &&
createPortal( createPortal(
<div className="nb-action-page"> <div data-testid="action-page" className="nb-action-page">
<RecursionField <RecursionField
basePath={field.address} basePath={field.address}
schema={schema} schema={schema}

View File

@ -61,7 +61,7 @@ function TypeSelect(props) {
}); });
return ( return (
<Select {...props}> <Select data-testid="antd-select" {...props}>
{Object.keys(types).map((key) => ( {Object.keys(types).map((key) => (
<Select.Option key={key} value={key}> <Select.Option key={key} value={key}>
{types[key].title} {types[key].title}

View File

@ -202,7 +202,7 @@ export const AppendsTreeSelect: React.FC<AppendsTreeSelectProps> = (props) => {
} }
const { fullTitle } = optionsMap[value] ?? {}; const { fullTitle } = optionsMap[value] ?? {};
return ( return (
<Tag closable={closable && !disabled} onClose={onClose}> <Tag data-testid="antd-tag" closable={closable && !disabled} onClose={onClose}>
{fullTitle?.join(' / ')} {fullTitle?.join(' / ')}
</Tag> </Tag>
); );
@ -219,6 +219,7 @@ export const AppendsTreeSelect: React.FC<AppendsTreeSelectProps> = (props) => {
return ( return (
<TreeSelect <TreeSelect
data-testid="antd-tree-select"
value={filteredValue} value={filteredValue}
placeholder={t('Select field')} placeholder={t('Select field')}
showCheckedStrategy={TreeSelect.SHOW_ALL} showCheckedStrategy={TreeSelect.SHOW_ALL}

View File

@ -23,9 +23,13 @@ import { flatData, getLabelFormatValue, useLabelUiSchema } from './util';
const useTableSelectorProps = () => { const useTableSelectorProps = () => {
const field: any = useField(); const field: any = useField();
const { multiple, options = [], setSelectedRows, selectedRows: rcSelectRows = [], onChange } = useContext( const {
RecordPickerContext, multiple,
); options = [],
setSelectedRows,
selectedRows: rcSelectRows = [],
onChange,
} = useContext(RecordPickerContext);
const { onRowSelectionChange, rowKey = 'id', ...others } = useTsp(); const { onRowSelectionChange, rowKey = 'id', ...others } = useTsp();
const { setVisible } = useActionContext(); const { setVisible } = useActionContext();
return { return {
@ -128,6 +132,7 @@ export const InternalPicker = observer(
<Input.Group compact style={{ display: 'flex', lineHeight: '32px' }}> <Input.Group compact style={{ display: 'flex', lineHeight: '32px' }}>
<div style={{ width: '100%' }}> <div style={{ width: '100%' }}>
<Select <Select
data-testid="antd-select"
style={{ width: '100%' }} style={{ width: '100%' }}
popupMatchSelectWidth={false} popupMatchSelectWidth={false}
{...others} {...others}

View File

@ -1,12 +1,12 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { FormLayout } from '@formily/antd-v5'; import { FormLayout } from '@formily/antd-v5';
import { RecursionField, observer, useField, useFieldSchema, SchemaOptionsContext } from '@formily/react'; import { RecursionField, SchemaOptionsContext, observer, useField, useFieldSchema } from '@formily/react';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { ACLCollectionProvider, useACLActionParamsContext } from '../../../acl';
import { CollectionProvider } from '../../../collection-manager'; import { CollectionProvider } from '../../../collection-manager';
import { useSchemaOptionsContext } from '../../../schema-component'; import { useSchemaOptionsContext } from '../../../schema-component';
import Select from '../select/Select'; import Select from '../select/Select';
import { useAssociationFieldContext, useInsertSchema } from './hooks'; import { useAssociationFieldContext, useInsertSchema } from './hooks';
import { ACLCollectionProvider, useACLActionParamsContext } from '../../../acl';
import schema from './schema'; import schema from './schema';
export const InternalSubTable = observer( export const InternalSubTable = observer(
@ -24,8 +24,8 @@ export const InternalSubTable = observer(
const option = useSchemaOptionsContext(); const option = useSchemaOptionsContext();
const components = { const components = {
...option.components, ...option.components,
'Radio.Group': Select, 'Radio.Group': (props) => <Select data-testid="antd-select" {...props} />,
'Checkbox.Group': (props) => <Select multiple={true} mode="multiple" {...props} />, 'Checkbox.Group': (props) => <Select data-testid="antd-select" multiple={true} mode="multiple" {...props} />,
}; };
return ( return (
<CollectionProvider name={options.target}> <CollectionProvider name={options.target}>

View File

@ -79,6 +79,7 @@ export const AssociationFilterFilterBlockInitializer = () => {
return ( return (
<SchemaInitializer.Button <SchemaInitializer.Button
data-testid="configure-fields-button-of-association-filter-filter-block"
className={css` className={css`
margin-top: 16px; margin-top: 16px;
`} `}

View File

@ -49,6 +49,7 @@ export const AssociationFilterInitializer = () => {
return ( return (
<SchemaInitializer.Button <SchemaInitializer.Button
data-testid="configure-fields-button-of-association-filter"
className={css` className={css`
margin-top: 16px; margin-top: 16px;
`} `}

View File

@ -1,6 +1,6 @@
import { APIClientProvider, AssociationSelect, FormProvider, SchemaComponent } from '@nocobase/client'; import { APIClientProvider, AssociationSelect, FormProvider, SchemaComponent } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { mockAPIClient } from '../../../../test'; import { mockAPIClient } from '../../../../testUtils';
const { apiClient, mockRequest } = mockAPIClient(); const { apiClient, mockRequest } = mockAPIClient();
mockRequest.onGet('/posts:list').reply(() => { mockRequest.onGet('/posts:list').reply(() => {

View File

@ -15,7 +15,7 @@ export const ViewSelect = observer(
} = useContext(CalendarToolbarContext); } = useContext(CalendarToolbarContext);
return ( return (
<div className="ant-btn-group"> <div className="ant-btn-group">
<Select popupMatchSelectWidth={false} value={view} onChange={onView}> <Select data-testid="antd-select" popupMatchSelectWidth={false} value={view} onChange={onView}>
{views.map((name) => ( {views.map((name) => (
<Select.Option key={name} value={name}> <Select.Option key={name} value={name}>
{messages[name]} {messages[name]}

View File

@ -45,6 +45,7 @@ export const CollectionSelect = connect(
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Select <Select
data-testid="antd-select"
placeholder={t('Select collection')} placeholder={t('Select collection')}
popupMatchSelectWidth={false} popupMatchSelectWidth={false}
{...others} {...others}

View File

@ -2,7 +2,7 @@ import { FormItem } from '@formily/antd-v5';
import { CollectionManagerProvider, CollectionSelect, FormProvider, SchemaComponent } from '@nocobase/client'; import { CollectionManagerProvider, CollectionSelect, FormProvider, SchemaComponent } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { collections } from '../../../../test'; import { collections } from '../../../../testUtils';
const schema = { const schema = {
type: 'object', type: 'object',

View File

@ -23,7 +23,7 @@ export const ColorSelect = connect(
(props) => { (props) => {
const compile = useCompile(); const compile = useCompile();
return ( return (
<Select {...props}> <Select data-testid="antd-select" {...props}>
{Object.keys(colors).map((color) => ( {Object.keys(colors).map((color) => (
<Select.Option value={color}> <Select.Option value={color}>
<Tag color={color}>{compile(colors[color] || colors.default)}</Tag> <Tag color={color}>{compile(colors[color] || colors.default)}</Tag>

View File

@ -64,6 +64,7 @@ const CronSetInternal = (props: CronSetProps) => {
return ( return (
<fieldset> <fieldset>
<Select <Select
data-testid="antd-select"
popupMatchSelectWidth={false} popupMatchSelectWidth={false}
allowClear allowClear
{...props} {...props}

View File

@ -50,13 +50,15 @@ export const DynamicComponent = (props) => {
}; };
return ( return (
<FormContext.Provider value={form}> <FormContext.Provider value={form}>
{component <div data-testid="dynamic-component-filter-item">
? React.createElement(component, { {component
value: props.value, ? React.createElement(component, {
onChange: props?.onChange, value: props.value,
renderSchemaComponent, onChange: props?.onChange,
}) renderSchemaComponent,
: renderSchemaComponent()} })
: renderSchemaComponent()}
</div>
</FormContext.Provider> </FormContext.Provider>
); );
}; };

View File

@ -42,7 +42,7 @@ export const FilterGroup = connect((props) => {
} }
> >
{remove && !mergedDisabled && ( {remove && !mergedDisabled && (
<a> <a data-testid="close-icon-button">
<CloseCircleOutlined <CloseCircleOutlined
style={{ style={{
position: 'absolute', position: 'absolute',
@ -58,6 +58,7 @@ export const FilterGroup = connect((props) => {
<Trans> <Trans>
{'Meet '} {'Meet '}
<Select <Select
data-testid="antd-select"
style={{ width: 'auto' }} style={{ width: 'auto' }}
value={logic} value={logic}
onChange={(value) => { onChange={(value) => {

View File

@ -5,8 +5,8 @@ import { Cascader, Select, Space } from 'antd';
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useCompile } from '../..'; import { useCompile } from '../..';
import { RemoveConditionContext } from './context';
import { DynamicComponent } from './DynamicComponent'; import { DynamicComponent } from './DynamicComponent';
import { RemoveConditionContext } from './context';
import { useValues } from './useValues'; import { useValues } from './useValues';
export const FilterItem = observer( export const FilterItem = observer(
@ -20,6 +20,7 @@ export const FilterItem = observer(
<div style={{ marginBottom: 8 }} className="nc-filter-item"> <div style={{ marginBottom: 8 }} className="nc-filter-item">
<Space> <Space>
<Cascader <Cascader
data-testid="antd-cascader"
className={css` className={css`
width: 160px; width: 160px;
`} `}
@ -37,6 +38,7 @@ export const FilterItem = observer(
placeholder={t('Select field')} placeholder={t('Select field')}
/> />
<Select <Select
data-testid="antd-select"
className={css` className={css`
min-width: 110px; min-width: 110px;
`} `}
@ -50,7 +52,7 @@ export const FilterItem = observer(
/> />
{!operator?.noValue ? <DynamicComponent value={value} schema={schema} onChange={setValue} /> : null} {!operator?.noValue ? <DynamicComponent value={value} schema={schema} onChange={setValue} /> : null}
{!props.disabled && ( {!props.disabled && (
<a> <a data-testid="close-icon-button">
<CloseCircleOutlined onClick={() => remove()} style={{ color: '#bfbfbf' }} /> <CloseCircleOutlined onClick={() => remove()} style={{ color: '#bfbfbf' }} />
</a> </a>
)} )}

View File

@ -462,6 +462,7 @@ FormItem.Designer = function Designer() {
<div style={{ alignItems: 'center', display: 'flex', justifyContent: 'space-between' }}> <div style={{ alignItems: 'center', display: 'flex', justifyContent: 'space-between' }}>
{t('Popup size')} {t('Popup size')}
<Select <Select
data-testid="antd-select"
bordered={false} bordered={false}
options={[ options={[
{ label: t('Small'), value: 'small' }, { label: t('Small'), value: 'small' },

View File

@ -7,7 +7,7 @@ import {
SchemaComponent, SchemaComponent,
} from '@nocobase/client'; } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { mockAPIClient } from '../../../../test'; import { mockAPIClient } from '../../../../testUtils';
const { apiClient, mockRequest } = mockAPIClient(); const { apiClient, mockRequest } = mockAPIClient();
mockRequest.onGet('/auth:check').reply(() => { mockRequest.onGet('/auth:check').reply(() => {

View File

@ -157,6 +157,7 @@ export const Templates = ({ style = {}, form }) => {
<Space wrap> <Space wrap>
<label style={labelStyle}>{t('Data template')}: </label> <label style={labelStyle}>{t('Data template')}: </label>
<Select <Select
data-testid="antd-select"
popupMatchSelectWidth={false} popupMatchSelectWidth={false}
options={templateOptions} options={templateOptions}
fieldNames={{ label: 'title', value: 'key' }} fieldNames={{ label: 'title', value: 'key' }}

View File

@ -16,7 +16,7 @@ import {
import { notification } from 'antd'; import { notification } from 'antd';
import React from 'react'; import React from 'react';
import { useFilterByTk } from '../../../../block-provider/BlockProvider'; import { useFilterByTk } from '../../../../block-provider/BlockProvider';
import { mockAPIClient } from '../../../../test'; import { mockAPIClient } from '../../../../testUtils';
import collections from './collections'; import collections from './collections';
const { apiClient, mockRequest } = mockAPIClient(); const { apiClient, mockRequest } = mockAPIClient();

View File

@ -18,7 +18,7 @@ import {
import { notification } from 'antd'; import { notification } from 'antd';
import React from 'react'; import React from 'react';
import { useFilterByTk } from '../../../../block-provider/BlockProvider'; import { useFilterByTk } from '../../../../block-provider/BlockProvider';
import { mockAPIClient } from '../../../../test'; import { mockAPIClient } from '../../../../testUtils';
import collections from './collections'; import collections from './collections';
const { apiClient, mockRequest } = mockAPIClient(); const { apiClient, mockRequest } = mockAPIClient();

View File

@ -16,7 +16,7 @@ import {
SchemaComponentProvider, SchemaComponentProvider,
} from '@nocobase/client'; } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { mockAPIClient } from '../../../../test'; import { mockAPIClient } from '../../../../testUtils';
import collections from './collections'; import collections from './collections';
const { apiClient, mockRequest } = mockAPIClient(); const { apiClient, mockRequest } = mockAPIClient();

View File

@ -7,7 +7,7 @@ import {
SchemaComponentProvider, SchemaComponentProvider,
} from '@nocobase/client'; } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { mockAPIClient } from '../../../../test'; import { mockAPIClient } from '../../../../testUtils';
const { apiClient, mockRequest } = mockAPIClient(); const { apiClient, mockRequest } = mockAPIClient();

View File

@ -42,7 +42,7 @@ const InternalGridCardBlockProvider = (props) => {
export const GridCardBlockProvider = (props) => { export const GridCardBlockProvider = (props) => {
return ( return (
<BlockProvider {...props}> <BlockProvider data-testid="grid-card-block" {...props}>
<InternalGridCardBlockProvider {...props} /> <InternalGridCardBlockProvider {...props} />
</BlockProvider> </BlockProvider>
); );

View File

@ -12,7 +12,7 @@ import {
} from '@nocobase/client'; } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { mockAPIClient } from '../../../../test'; import { mockAPIClient } from '../../../../testUtils';
const { apiClient, mockRequest } = mockAPIClient(); const { apiClient, mockRequest } = mockAPIClient();
mockRequest.onGet('/auth:check').reply(() => { mockRequest.onGet('/auth:check').reply(() => {

View File

@ -9,7 +9,7 @@ import {
SchemaComponentProvider, SchemaComponentProvider,
} from '@nocobase/client'; } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { mockAPIClient } from '../../../../test'; import { mockAPIClient } from '../../../../testUtils';
import collections from './collections'; import collections from './collections';
import data from './data'; import data from './data';

View File

@ -33,16 +33,14 @@ const InternalListBlockProvider = (props) => {
<FormContext.Provider value={form}> <FormContext.Provider value={form}>
<FormLayout layout={'vertical'}> <FormLayout layout={'vertical'}>
<div <div
className={cx( className={cx(css`
css` .ant-description-input {
.ant-description-input { line-height: 34px;
line-height: 34px; }
} .ant-formily-item-feedback-layout-loose {
.ant-formily-item-feedback-layout-loose { display: inline;
display: inline; }
} `)}
`,
)}
> >
{props.children} {props.children}
</div> </div>
@ -54,7 +52,7 @@ const InternalListBlockProvider = (props) => {
export const ListBlockProvider = (props) => { export const ListBlockProvider = (props) => {
return ( return (
<BlockProvider {...props}> <BlockProvider data-testid="list-block" {...props}>
<InternalListBlockProvider {...props} /> <InternalListBlockProvider {...props} />
</BlockProvider> </BlockProvider>
); );

View File

@ -20,7 +20,10 @@ export const PageDesigner = ({ title }) => {
<div className={'general-schema-designer'}> <div className={'general-schema-designer'}>
<div className={'general-schema-designer-icons'}> <div className={'general-schema-designer-icons'}>
<Space size={2} align={'center'}> <Space size={2} align={'center'}>
<SchemaSettings title={<MenuOutlined style={{ cursor: 'pointer', fontSize: 12 }} />}> <SchemaSettings
data-testid="page-designer-button"
title={<MenuOutlined style={{ cursor: 'pointer', fontSize: 12 }} />}
>
<SchemaSettings.SwitchItem <SchemaSettings.SwitchItem
title={t('Enable page header')} title={t('Enable page header')}
checked={!fieldSchema['x-component-props']?.disablePageHeader} checked={!fieldSchema['x-component-props']?.disablePageHeader}

View File

@ -175,6 +175,7 @@ export const InputRecordPicker: React.FC<any> = (props: IRecordPickerProps) => {
/> />
) : ( ) : (
<Select <Select
data-testid="antd-select"
{...others} {...others}
mode={multiple ? 'multiple' : props.mode} mode={multiple ? 'multiple' : props.mode}
fieldNames={fieldNames} fieldNames={fieldNames}

View File

@ -19,7 +19,7 @@ import {
TableV2, TableV2,
} from '@nocobase/client'; } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { mainCollections, mockAPIClient } from '../../../../test'; import { mainCollections, mockAPIClient } from '../../../../testUtils';
import data from './mockData'; import data from './mockData';
const { apiClient, mockRequest } = mockAPIClient(); const { apiClient, mockRequest } = mockAPIClient();

View File

@ -1,6 +1,6 @@
import { APIClientProvider, FormProvider, RemoteSelect, SchemaComponent } from '@nocobase/client'; import { APIClientProvider, FormProvider, RemoteSelect, SchemaComponent } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { mockAPIClient } from '../../../../test'; import { mockAPIClient } from '../../../../testUtils';
const { apiClient, mockRequest } = mockAPIClient(); const { apiClient, mockRequest } = mockAPIClient();
mockRequest.onGet('/posts:list').reply(() => { mockRequest.onGet('/posts:list').reply(() => {

View File

@ -43,6 +43,7 @@ const ObjectSelect = (props: Props) => {
return ( return (
<AntdSelect <AntdSelect
data-testid="antd-select"
value={toValue(value)} value={toValue(value)}
defaultValue={toValue(defaultValue)} defaultValue={toValue(defaultValue)}
allowClear allowClear
@ -108,6 +109,7 @@ const InternalSelect = connect(
}; };
return ( return (
<AntdSelect <AntdSelect
data-testid="antd-select"
showSearch showSearch
filterOption={filterOption} filterOption={filterOption}
allowClear allowClear

View File

@ -12,7 +12,7 @@ import {
import { notification } from 'antd'; import { notification } from 'antd';
import { range } from 'lodash'; import { range } from 'lodash';
import React from 'react'; import React from 'react';
import { mockAPIClient } from '../../../../test'; import { mockAPIClient } from '../../../../testUtils';
import collections from './collections'; import collections from './collections';
const { apiClient, mockRequest } = mockAPIClient(); const { apiClient, mockRequest } = mockAPIClient();

View File

@ -1,4 +1,4 @@
import { mockAPIClient } from '../../../../test'; import { mockAPIClient } from '../../../../testUtils';
const sleep = (value: number) => new Promise((resolve) => setTimeout(resolve, value)); const sleep = (value: number) => new Promise((resolve) => setTimeout(resolve, value));
const { apiClient, mockRequest } = mockAPIClient(); const { apiClient, mockRequest } = mockAPIClient();

View File

@ -77,6 +77,7 @@ const ConstantTypes = {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Select <Select
data-testid="antd-select"
value={value} value={value}
onChange={onChange} onChange={onChange}
placeholder={t('Select')} placeholder={t('Select')}

View File

@ -93,12 +93,7 @@ export const SortableItem: React.FC<SortableItemProps> = observer(
export const DragHandler = (props) => { export const DragHandler = (props) => {
const { draggable } = useContext(SortableContext); const { draggable } = useContext(SortableContext);
const { isDragging, attributes, listeners, setNodeRef, transform } = draggable; const { attributes, listeners, setNodeRef } = draggable;
const style = transform
? {
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
}
: undefined;
return ( return (
<div <div
@ -118,8 +113,6 @@ export const DragHandler = (props) => {
zIndex: 1, zIndex: 1,
// backgroundColor: '#333', // backgroundColor: '#333',
lineHeight: 0, lineHeight: 0,
height: 2,
width: 2,
fontSize: 0, fontSize: 0,
display: 'inline-block', display: 'inline-block',
}} }}

View File

@ -5,6 +5,7 @@ import classNames from 'classnames';
// @ts-ignore // @ts-ignore
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { useCollection } from '../collection-manager';
import { useCollectMenuItem, useMenuItem } from '../hooks/useMenuItem'; import { useCollectMenuItem, useMenuItem } from '../hooks/useMenuItem';
import { Icon } from '../icon'; import { Icon } from '../icon';
import { SchemaComponent, useActionContext } from '../schema-component'; import { SchemaComponent, useActionContext } from '../schema-component';
@ -251,6 +252,7 @@ SchemaInitializer.Button = observer(
const { Component: CollectComponent, getMenuItem, clean } = useMenuItem(); const { Component: CollectComponent, getMenuItem, clean } = useMenuItem();
const menuItems = useRef([]); const menuItems = useRef([]);
const { styles } = useStyles(); const { styles } = useStyles();
const { name } = useCollection();
const changeMenu = (v: boolean) => { const changeMenu = (v: boolean) => {
setVisible(v); setVisible(v);
@ -266,6 +268,10 @@ SchemaInitializer.Button = observer(
return null; return null;
} }
if (others['data-testid'] && name) {
others['data-testid'] = `${others['data-testid']}-${name}`;
}
const buttonDom = component || ( const buttonDom = component || (
<Button <Button
type={'dashed'} type={'dashed'}
@ -514,7 +520,7 @@ SchemaInitializer.ActionModal = function ActionModal(props: SchemaInitializerAct
async run() { async run() {
await onCancel?.(); await onCancel?.();
ctx.setVisible(false); ctx.setVisible(false);
form.reset(); void form.reset();
}, },
}; };
}, [onCancel]); }, [onCancel]);
@ -529,7 +535,7 @@ SchemaInitializer.ActionModal = function ActionModal(props: SchemaInitializerAct
await form.validate(); await form.validate();
await onSubmit?.(form.values); await onSubmit?.(form.values);
ctx.setVisible(false); ctx.setVisible(false);
form.reset(); void form.reset();
}, },
}; };
}, [onSubmit]); }, [onSubmit]);

View File

@ -2,6 +2,7 @@ import { gridRowColWrap } from '../utils';
// 页面里添加区块 // 页面里添加区块
export const BlockInitializers = { export const BlockInitializers = {
'data-testid': 'add-block-button-in-page',
title: '{{t("Add block")}}', title: '{{t("Add block")}}',
icon: 'PlusOutlined', icon: 'PlusOutlined',
wrap: gridRowColWrap, wrap: gridRowColWrap,

View File

@ -14,6 +14,7 @@ export const BulkEditFormItemInitializers = (props: any) => {
const associationFields = useAssociatedFormItemInitializerFields({ readPretty: true, block: 'Form' }); const associationFields = useAssociatedFormItemInitializerFields({ readPretty: true, block: 'Form' });
return ( return (
<SchemaInitializer.Button <SchemaInitializer.Button
data-testid="configure-fields-button-of-bulk-edit-form-item"
wrap={gridRowColWrap} wrap={gridRowColWrap}
icon={'SettingOutlined'} icon={'SettingOutlined'}
items={union<any>( items={union<any>(

View File

@ -2,6 +2,7 @@ import { useCollection } from '../../';
// 日历的操作配置 // 日历的操作配置
export const CalendarActionInitializers = { export const CalendarActionInitializers = {
'data-testid': 'configure-actions-button-of-calendar-block',
title: '{{t("Configure actions")}}', title: '{{t("Configure actions")}}',
icon: 'SettingOutlined', icon: 'SettingOutlined',
style: { marginLeft: 8 }, style: { marginLeft: 8 },

View File

@ -2,6 +2,7 @@ import { useCollection } from '../..';
// 表单的操作配置 // 表单的操作配置
export const CalendarFormActionInitializers = { export const CalendarFormActionInitializers = {
'data-testid': 'configure-actions-button-of-calendar-form',
title: '{{t("Configure actions")}}', title: '{{t("Configure actions")}}',
icon: 'SettingOutlined', icon: 'SettingOutlined',
style: { style: {

View File

@ -34,6 +34,7 @@ export const CustomFormItemInitializers = (props: any) => {
} }
return ( return (
<SchemaInitializer.Button <SchemaInitializer.Button
data-testid="configure-fields-button-of-custom-form-item"
wrap={gridRowColWrap} wrap={gridRowColWrap}
icon={'SettingOutlined'} icon={'SettingOutlined'}
items={fieldItems} items={fieldItems}

View File

@ -1,5 +1,6 @@
// 表单的操作配置 // 表单的操作配置
export const DetailsActionInitializers = { export const DetailsActionInitializers = {
'data-testid': 'configure-actions-button-of-details-block',
title: '{{t("Configure actions")}}', title: '{{t("Configure actions")}}',
icon: 'SettingOutlined', icon: 'SettingOutlined',
style: { style: {

View File

@ -1,5 +1,6 @@
// 表单的操作配置 // 表单的操作配置
export const FilterFormActionInitializers = { export const FilterFormActionInitializers = {
'data-testid': 'configure-actions-button-of-filter-form',
title: '{{t("Configure actions")}}', title: '{{t("Configure actions")}}',
icon: 'SettingOutlined', icon: 'SettingOutlined',
items: [ items: [

View File

@ -26,6 +26,7 @@ const FormTriggerWorkflowActionInitializer = {
// 表单的操作配置 // 表单的操作配置
export const FormActionInitializers = { export const FormActionInitializers = {
'data-testid': 'configure-actions-button-of-form-block',
title: '{{t("Configure actions")}}', title: '{{t("Configure actions")}}',
icon: 'SettingOutlined', icon: 'SettingOutlined',
items: [ items: [
@ -159,6 +160,7 @@ export const FormActionInitializers = {
}; };
export const CreateFormActionInitializers = { export const CreateFormActionInitializers = {
'data-testid': 'configure-actions-button-of-create-form',
title: '{{t("Configure actions")}}', title: '{{t("Configure actions")}}',
icon: 'SettingOutlined', icon: 'SettingOutlined',
items: [ items: [
@ -292,6 +294,7 @@ export const CreateFormActionInitializers = {
}; };
export const UpdateFormActionInitializers = { export const UpdateFormActionInitializers = {
'data-testid': 'configure-actions-button-of-update-form',
title: '{{t("Configure actions")}}', title: '{{t("Configure actions")}}',
icon: 'SettingOutlined', icon: 'SettingOutlined',
items: [ items: [
@ -424,6 +427,7 @@ export const UpdateFormActionInitializers = {
}; };
export const BulkEditFormActionInitializers = { export const BulkEditFormActionInitializers = {
'data-testid': 'configure-actions-button-of-bulk-edit-form',
title: '{{t("Configure actions")}}', title: '{{t("Configure actions")}}',
icon: 'SettingOutlined', icon: 'SettingOutlined',
items: [ items: [

View File

@ -78,6 +78,7 @@ export const FormItemInitializers = (props: any) => {
); );
return ( return (
<SchemaInitializer.Button <SchemaInitializer.Button
data-testid="configure-fields-button-of-form-item"
wrap={gridRowColWrap} wrap={gridRowColWrap}
icon={'SettingOutlined'} icon={'SettingOutlined'}
items={fieldItems} items={fieldItems}
@ -151,6 +152,7 @@ export const FilterFormItemInitializers = (props: any) => {
); );
return ( return (
<SchemaInitializer.Button <SchemaInitializer.Button
data-testid="configure-fields-button-of-filter-form-item"
wrap={gridRowColWrap} wrap={gridRowColWrap}
icon={'SettingOutlined'} icon={'SettingOutlined'}
items={fieldItems} items={fieldItems}

View File

@ -2,6 +2,7 @@ import { useCollection } from '../../collection-manager';
// 表单的操作配置 // 表单的操作配置
export const GridCardActionInitializers = { export const GridCardActionInitializers = {
'data-testid': 'configure-actions-button-of-grid-card-block',
title: "{{t('Configure actions')}}", title: "{{t('Configure actions')}}",
icon: 'SettingOutlined', icon: 'SettingOutlined',
style: { style: {
@ -139,6 +140,7 @@ export const GridCardActionInitializers = {
}; };
export const GridCardItemActionInitializers = { export const GridCardItemActionInitializers = {
'data-testid': 'configure-actions-button-of-grid-card-item',
title: '{{t("Configure actions")}}', title: '{{t("Configure actions")}}',
icon: 'SettingOutlined', icon: 'SettingOutlined',
items: [ items: [

View File

@ -1,6 +1,7 @@
import { useCollection } from '../../'; import { useCollection } from '../../';
export const KanbanActionInitializers = { export const KanbanActionInitializers = {
'data-testid': 'configure-actions-button-of-kanban-block',
title: "{{t('Configure actions')}}", title: "{{t('Configure actions')}}",
icon: 'SettingOutlined', icon: 'SettingOutlined',
style: { style: {

View File

@ -2,6 +2,7 @@ import { useCollection } from '../../collection-manager';
// 表单的操作配置 // 表单的操作配置
export const ListActionInitializers = { export const ListActionInitializers = {
'data-testid': 'configure-actions-button-of-list-block',
title: "{{t('Configure actions')}}", title: "{{t('Configure actions')}}",
icon: 'SettingOutlined', icon: 'SettingOutlined',
style: { style: {
@ -143,6 +144,7 @@ export const ListActionInitializers = {
}; };
export const ListItemActionInitializers = { export const ListItemActionInitializers = {
'data-testid': 'configure-actions-button-of-list-item',
title: '{{t("Configure actions")}}', title: '{{t("Configure actions")}}',
icon: 'SettingOutlined', icon: 'SettingOutlined',
items: [ items: [

View File

@ -6,6 +6,7 @@ const useVisibleCollection = () => {
}; };
// 表单的操作配置 // 表单的操作配置
export const ReadPrettyFormActionInitializers = { export const ReadPrettyFormActionInitializers = {
'data-testid': 'configure-actions-button-of-read-pretty-form',
title: '{{t("Configure actions")}}', title: '{{t("Configure actions")}}',
icon: 'SettingOutlined', icon: 'SettingOutlined',
style: { style: {

View File

@ -1,6 +1,6 @@
import { union } from 'lodash';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useCompile } from '../../schema-component';
import { SchemaInitializer } from '../SchemaInitializer'; import { SchemaInitializer } from '../SchemaInitializer';
import { import {
gridRowColWrap, gridRowColWrap,
@ -8,7 +8,6 @@ import {
useFormItemInitializerFields, useFormItemInitializerFields,
useInheritsFormItemInitializerFields, useInheritsFormItemInitializerFields,
} from '../utils'; } from '../utils';
import { useCompile } from '../../schema-component';
export const ReadPrettyFormItemInitializers = (props: any) => { export const ReadPrettyFormItemInitializers = (props: any) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -72,6 +71,7 @@ export const ReadPrettyFormItemInitializers = (props: any) => {
); );
return ( return (
<SchemaInitializer.Button <SchemaInitializer.Button
data-testid="configure-fields-button-of-read-pretty-form-item"
wrap={gridRowColWrap} wrap={gridRowColWrap}
icon={'SettingOutlined'} icon={'SettingOutlined'}
items={fieldItems} items={fieldItems}

View File

@ -7,6 +7,7 @@ export const RecordFormBlockInitializers = (props: any) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<SchemaInitializer.Button <SchemaInitializer.Button
data-testid="add-block-button-in-record-form-block"
wrap={gridRowColWrap} wrap={gridRowColWrap}
title={t('Add block')} title={t('Add block')}
icon={'PlusOutlined'} icon={'PlusOutlined'}

View File

@ -1,5 +1,6 @@
// 表格操作配置 // 表格操作配置
export const SubTableActionInitializers = { export const SubTableActionInitializers = {
'data-testid': 'configure-actions-button-of-sub-table',
title: "{{t('Configure actions')}}", title: "{{t('Configure actions')}}",
icon: 'SettingOutlined', icon: 'SettingOutlined',
style: { style: {

View File

@ -3,6 +3,7 @@ import { useCollection } from '../../';
// 表格操作配置 // 表格操作配置
export const TableActionInitializers = { export const TableActionInitializers = {
'data-testid': 'configure-actions-button-of-table-block',
title: "{{t('Configure actions')}}", title: "{{t('Configure actions')}}",
icon: 'SettingOutlined', icon: 'SettingOutlined',
style: { style: {

View File

@ -1,6 +1,7 @@
import { useField, useFieldSchema } from '@formily/react'; import { useField, useFieldSchema } from '@formily/react';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useCollection } from '../../collection-manager';
import { useCompile } from '../../schema-component'; import { useCompile } from '../../schema-component';
import { SchemaInitializer } from '../SchemaInitializer'; import { SchemaInitializer } from '../SchemaInitializer';
import { import {
@ -12,7 +13,8 @@ import {
// 表格列配置 // 表格列配置
export const TableColumnInitializers = (props: any) => { export const TableColumnInitializers = (props: any) => {
const { items = [], action = true } = props; const { action = true } = props;
const { name } = useCollection();
const { t } = useTranslation(); const { t } = useTranslation();
const field = useField(); const field = useField();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
@ -69,6 +71,7 @@ export const TableColumnInitializers = (props: any) => {
return ( return (
<SchemaInitializer.Button <SchemaInitializer.Button
data-testid={`configure-columns-button-of-table-block`}
insertPosition={'beforeEnd'} insertPosition={'beforeEnd'}
icon={'SettingOutlined'} icon={'SettingOutlined'}
wrap={(s) => { wrap={(s) => {

View File

@ -9,6 +9,7 @@ export const TableSelectorInitializers = (props: any) => {
return ( return (
<SchemaInitializer.Button <SchemaInitializer.Button
data-testid="add-block-button-in-table-selector"
wrap={gridRowColWrap} wrap={gridRowColWrap}
title={component ? null : t('Add block')} title={component ? null : t('Add block')}
icon={'PlusOutlined'} icon={'PlusOutlined'}

View File

@ -127,7 +127,7 @@ export const BulkEditField = (props: any) => {
} }
`} `}
> >
<Select defaultValue={type} value={type} onChange={typeChangeHandler}> <Select data-testid="antd-select" defaultValue={type} value={type} onChange={typeChangeHandler}>
<Select.Option value={BulkEditFormItemValueType.RemainsTheSame}>{t('Remains the same')}</Select.Option> <Select.Option value={BulkEditFormItemValueType.RemainsTheSame}>{t('Remains the same')}</Select.Option>
<Select.Option value={BulkEditFormItemValueType.ChangedTo}>{t('Changed to')}</Select.Option> <Select.Option value={BulkEditFormItemValueType.ChangedTo}>{t('Changed to')}</Select.Option>
<Select.Option value={BulkEditFormItemValueType.Clear}>{t('Clear')}</Select.Option> <Select.Option value={BulkEditFormItemValueType.Clear}>{t('Clear')}</Select.Option>

View File

@ -1,9 +1,9 @@
import React from 'react';
import { useField, useFieldSchema } from '@formily/react'; import { useField, useFieldSchema } from '@formily/react';
import { SchemaSettings } from '../schema-settings';
import { useTranslation } from 'react-i18next';
import { Select } from 'antd'; import { Select } from 'antd';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useDesignable } from '../schema-component'; import { useDesignable } from '../schema-component';
import { SchemaSettings } from '../schema-settings';
interface Options { interface Options {
openMode?: boolean; openMode?: boolean;
@ -49,6 +49,7 @@ export const OpenModeSchemaItems: React.FC<Options> = (options) => {
<div style={{ alignItems: 'center', display: 'flex', justifyContent: 'space-between' }}> <div style={{ alignItems: 'center', display: 'flex', justifyContent: 'space-between' }}>
{t('Popup size')} {t('Popup size')}
<Select <Select
data-testid="antd-select"
bordered={false} bordered={false}
options={[ options={[
{ label: t('Small'), value: 'small' }, { label: t('Small'), value: 'small' },

View File

@ -171,6 +171,7 @@ function SelectItem(props) {
<div style={{ alignItems: 'center', display: 'flex', justifyContent: 'space-between' }}> <div style={{ alignItems: 'center', display: 'flex', justifyContent: 'space-between' }}>
{title} {title}
<Select <Select
data-testid="antd-select"
popupMatchSelectWidth={false} popupMatchSelectWidth={false}
bordered={false} bordered={false}
value={value} value={value}

View File

@ -3,7 +3,8 @@ import { css } from '@emotion/css';
import { useField, useFieldSchema } from '@formily/react'; import { useField, useFieldSchema } from '@formily/react';
import { Space } from 'antd'; import { Space } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import React, { useMemo } from 'react'; import _ from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { DragHandler, useCompile, useDesignable, useGridContext, useGridRowContext } from '../schema-component'; import { DragHandler, useCompile, useDesignable, useGridContext, useGridRowContext } from '../schema-component';
import { gridRowColWrap } from '../schema-initializer/utils'; import { gridRowColWrap } from '../schema-initializer/utils';
@ -35,7 +36,72 @@ const overrideAntdCSS = css`
& .ant-space-item .anticon { & .ant-space-item .anticon {
margin: 0; margin: 0;
} }
&:hover {
display: block !important;
}
`; `;
const DesignerControl = ({ onShow, children }) => {
const divRef = React.useRef(null);
const shouldUnmount = React.useRef(true);
const isVisible = React.useRef(false);
const unmount = useCallback(
_.debounce(() => {
if (shouldUnmount.current && !isVisible.current) {
onShow(false);
}
}, 300) as () => void,
[onShow],
);
useEffect(() => {
// 兼容旧浏览器
if (!IntersectionObserver) {
return onShow(true);
}
const observer = new IntersectionObserver((entries) => {
// 兼容旧浏览器
if (entries[0].isIntersecting === undefined) {
return onShow(true);
}
if (entries[0].isIntersecting) {
isVisible.current = true;
onShow(true);
} else {
isVisible.current = false;
unmount();
}
});
observer.observe(divRef.current);
return () => {
observer.disconnect();
};
}, [unmount, onShow]);
const onMouseEnter = useCallback(() => {
shouldUnmount.current = false;
}, []);
const onMouseLeave = useCallback(() => {
shouldUnmount.current = true;
unmount();
}, [unmount]);
return (
<div
ref={divRef}
className={classNames('general-schema-designer', overrideAntdCSS)}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{children}
</div>
);
};
export const GeneralSchemaDesigner = (props: any) => { export const GeneralSchemaDesigner = (props: any) => {
const { disableInitializer, title, template, draggable = true } = props; const { disableInitializer, title, template, draggable = true } = props;
const { dn, designable } = useDesignable(); const { dn, designable } = useDesignable();
@ -43,12 +109,15 @@ export const GeneralSchemaDesigner = (props: any) => {
const { t } = useTranslation(); const { t } = useTranslation();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const compile = useCompile(); const compile = useCompile();
const [visible, setVisible] = useState(false);
const schemaSettingsProps = { const schemaSettingsProps = {
dn, dn,
field, field,
fieldSchema, fieldSchema,
}; };
const onShow = useCallback((value) => setVisible(value), []);
const rowCtx = useGridRowContext(); const rowCtx = useGridRowContext();
const ctx = useGridContext(); const ctx = useGridContext();
const templateName = ['FormItem', 'ReadPrettyFormItem'].includes(template?.componentName) const templateName = ['FormItem', 'ReadPrettyFormItem'].includes(template?.componentName)
@ -58,9 +127,7 @@ export const GeneralSchemaDesigner = (props: any) => {
return { return {
insertPosition: 'afterEnd', insertPosition: 'afterEnd',
wrap: rowCtx?.cols?.length > 1 ? undefined : gridRowColWrap, wrap: rowCtx?.cols?.length > 1 ? undefined : gridRowColWrap,
component: ( component: <PlusOutlined data-testid="designer-add-block" style={{ cursor: 'pointer', fontSize: 14 }} />,
<PlusOutlined data-testid="GeneralSchemaDesigner-Initializer" style={{ cursor: 'pointer', fontSize: 14 }} />
),
}; };
}, [rowCtx?.cols?.length]); }, [rowCtx?.cols?.length]);
@ -69,45 +136,46 @@ export const GeneralSchemaDesigner = (props: any) => {
} }
return ( return (
<div className={classNames('general-schema-designer', overrideAntdCSS)}> <DesignerControl onShow={onShow}>
{title && ( {visible ? (
<div className={classNames('general-schema-designer-title', titleCss)}> <>
<Space size={2}> {title && (
<span className={'title-tag'}>{compile(title)}</span> <div className={classNames('general-schema-designer-title', titleCss)}>
{template && ( <Space size={2}>
<span className={'title-tag'}> <span className={'title-tag'}>{compile(title)}</span>
{t('Reference template')}: {templateName || t('Untitled')} {template && (
</span> <span className={'title-tag'}>
)} {t('Reference template')}: {templateName || t('Untitled')}
</Space> </span>
</div> )}
)} </Space>
<div className={'general-schema-designer-icons'}> </div>
<Space size={2} align={'center'}>
{draggable && (
<DragHandler>
<DragOutlined data-testid="GeneralSchemaDesigner-DragHandler" />
</DragHandler>
)} )}
{!disableInitializer && <div className={'general-schema-designer-icons'}>
(ctx?.InitializerComponent ? ( <Space size={2} align={'center'}>
<ctx.InitializerComponent {...initializerProps} /> {draggable && (
) : ( <DragHandler>
ctx?.renderSchemaInitializer?.(initializerProps) <DragOutlined data-testid="designer-drag" />
))} </DragHandler>
<SchemaSettings )}
title={ {!disableInitializer &&
<MenuOutlined (ctx?.InitializerComponent ? (
data-testid="GeneralSchemaDesigner-SchemaSettings" <ctx.InitializerComponent {...initializerProps} />
style={{ cursor: 'pointer', fontSize: 12 }} ) : (
/> ctx?.renderSchemaInitializer?.(initializerProps)
} ))}
{...schemaSettingsProps} <SchemaSettings
> title={
{props.children} <MenuOutlined data-testid="designer-schema-settings" style={{ cursor: 'pointer', fontSize: 12 }} />
</SchemaSettings> }
</Space> {...schemaSettingsProps}
</div> >
</div> {props.children}
</SchemaSettings>
</Space>
</div>
</>
) : null}
</DesignerControl>
); );
}; };

View File

@ -72,6 +72,7 @@ export const FormFieldLinkageRuleAction = observer(
placeholder={t('Select field')} placeholder={t('Select field')}
/> />
<Select <Select
data-testid="antd-select"
popupMatchSelectWidth={false} popupMatchSelectWidth={false}
value={operator} value={operator}
className={css` className={css`
@ -92,7 +93,7 @@ export const FormFieldLinkageRuleAction = observer(
/> />
)} )}
{!props.disabled && ( {!props.disabled && (
<a> <a data-testid="close-icon-button">
<CloseCircleOutlined onClick={() => remove()} style={{ color: '#bfbfbf' }} /> <CloseCircleOutlined onClick={() => remove()} style={{ color: '#bfbfbf' }} />
</a> </a>
)} )}
@ -141,7 +142,7 @@ export const FormButtonLinkageRuleAction = observer(
}, },
})} })}
{!props.disabled && ( {!props.disabled && (
<a> <a data-testid="close-icon-button">
<CloseCircleOutlined onClick={() => remove()} style={{ color: '#bfbfbf' }} /> <CloseCircleOutlined onClick={() => remove()} style={{ color: '#bfbfbf' }} />
</a> </a>
)} )}

View File

@ -1,9 +1,8 @@
import React, { useState } from 'react';
import { Input, Select } from 'antd'; import { Input, Select } from 'antd';
import { css } from '@emotion/css'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { DynamicComponent } from './DynamicComponent';
import { Variable } from '.././../schema-component'; import { Variable } from '.././../schema-component';
import { DynamicComponent } from './DynamicComponent';
import { useVariableOptions } from './Variables'; import { useVariableOptions } from './Variables';
const { Option } = Select; const { Option } = Select;
@ -16,6 +15,7 @@ export const ValueDynamicComponent = (props) => {
return ( return (
<Input.Group compact> <Input.Group compact>
<Select <Select
data-testid="antd-select"
value={mode} value={mode}
style={{ width: 150 }} style={{ width: 150 }}
onChange={(value) => { onChange={(value) => {

View File

@ -175,7 +175,7 @@ export const SchemaSettings: React.FC<SchemaSettingsProps> & SchemaSettingsNeste
`} `}
menu={{ items }} menu={{ items }}
> >
{typeof title === 'string' ? <span>{title}</span> : title} <div data-testid={props['data-testid']}>{typeof title === 'string' ? <span>{title}</span> : title}</div>
</Dropdown> </Dropdown>
</> </>
); );
@ -708,6 +708,7 @@ SchemaSettings.SelectItem = function SelectItem(props) {
<div style={{ alignItems: 'center', display: 'flex', justifyContent: 'space-between' }}> <div style={{ alignItems: 'center', display: 'flex', justifyContent: 'space-between' }}>
{title} {title}
<Select <Select
data-testid="antd-select"
popupMatchSelectWidth={false} popupMatchSelectWidth={false}
bordered={false} bordered={false}
defaultValue={value} defaultValue={value}

View File

@ -18,8 +18,5 @@ export const SystemSettingsProvider: React.FC<{ children?: ReactNode }> = (props
return render(); return render();
} }
// 主要是为了方便在 e2e 测试中获取到 adminSchemaUid
localStorage.setItem('NOCOBASE_SYSTEM_SETTINGS', JSON.stringify(result.data));
return <SystemSettingsContext.Provider value={result}>{props.children}</SystemSettingsContext.Provider>; return <SystemSettingsContext.Provider value={result}>{props.children}</SystemSettingsContext.Provider>;
}; };

View File

@ -1,4 +1,3 @@
export { default as collections } from './collections'; export { default as collections } from './collections';
export { mainCollections } from './mainCollections'; export { mainCollections } from './mainCollections';
export * from './mockAPIClient'; export * from './mockAPIClient';

View File

@ -172,6 +172,7 @@ export const CurrentUser = () => {
}} }}
> >
<span <span
data-testid="user-center-button"
className={css` className={css`
max-width: 160px; max-width: 160px;
overflow: hidden; overflow: hidden;

View File

@ -27,6 +27,7 @@ export const useLanguageSettings = () => {
> >
{t('Language')}{' '} {t('Language')}{' '}
<Select <Select
data-testid="antd-select"
popupMatchSelectWidth={false} popupMatchSelectWidth={false}
style={{ minWidth: 100 }} style={{ minWidth: 100 }}
bordered={false} bordered={false}

Some files were not shown because too many files have changed in this diff Show More