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",
"ecmaFeatures": {
"jsx": true
}
},
"project": "./tsconfig.json"
},
"rules": {
"@typescript-eslint/no-this-alias": "off",
@ -54,6 +55,7 @@
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/ban-ts-comment": "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/
es/
.env
.env.e2e
.DS_Store
yarn-error.log
lerna-debug.log
@ -30,4 +31,7 @@ storage/plugins
storage/tar
storage/tmp
storage/app.watch.ts
storage/logs-e2e
storage/uploads-e2e
tsconfig.paths.json
playwright

13
.vscode/launch.json vendored
View File

@ -37,6 +37,19 @@
"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",
"request": "launch",

View File

@ -25,6 +25,9 @@
"tar": "nocobase tar",
"test": "nocobase test",
"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",
"doc": "nocobase doc",
"postinstall": "nocobase postinstall",
@ -64,6 +67,8 @@
"@commitlint/cli": "^16.1.0",
"@commitlint/config-conventional": "^16.0.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/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
@ -76,6 +81,7 @@
"dumi-theme-nocobase": "^0.2.14",
"eslint-plugin-jest-dom": "^5.0.1",
"eslint-plugin-testing-library": "^5.11.0",
"execa": "^5.1.1",
"ghooks": "^2.0.4",
"jest": "^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 = () => {
return (
<Card bordered={false}>
<Card data-testid="acl-pane-card" bordered={false}>
<SchemaComponent components={components} schema={schema2} />
</Card>
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +1,12 @@
import { css } from '@emotion/css';
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 { ACLCollectionProvider, useACLActionParamsContext } from '../../../acl';
import { CollectionProvider } from '../../../collection-manager';
import { useSchemaOptionsContext } from '../../../schema-component';
import Select from '../select/Select';
import { useAssociationFieldContext, useInsertSchema } from './hooks';
import { ACLCollectionProvider, useACLActionParamsContext } from '../../../acl';
import schema from './schema';
export const InternalSubTable = observer(
@ -24,8 +24,8 @@ export const InternalSubTable = observer(
const option = useSchemaOptionsContext();
const components = {
...option.components,
'Radio.Group': Select,
'Checkbox.Group': (props) => <Select multiple={true} mode="multiple" {...props} />,
'Radio.Group': (props) => <Select data-testid="antd-select" {...props} />,
'Checkbox.Group': (props) => <Select data-testid="antd-select" multiple={true} mode="multiple" {...props} />,
};
return (
<CollectionProvider name={options.target}>

View File

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

View File

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

View File

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

View File

@ -15,7 +15,7 @@ export const ViewSelect = observer(
} = useContext(CalendarToolbarContext);
return (
<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) => (
<Select.Option key={name} value={name}>
{messages[name]}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,7 +20,10 @@ export const PageDesigner = ({ title }) => {
<div className={'general-schema-designer'}>
<div className={'general-schema-designer-icons'}>
<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
title={t('Enable page header')}
checked={!fieldSchema['x-component-props']?.disablePageHeader}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,6 +9,7 @@ export const TableSelectorInitializers = (props: any) => {
return (
<SchemaInitializer.Button
data-testid="add-block-button-in-table-selector"
wrap={gridRowColWrap}
title={component ? null : t('Add block')}
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.ChangedTo}>{t('Changed to')}</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 { SchemaSettings } from '../schema-settings';
import { useTranslation } from 'react-i18next';
import { Select } from 'antd';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useDesignable } from '../schema-component';
import { SchemaSettings } from '../schema-settings';
interface Options {
openMode?: boolean;
@ -49,6 +49,7 @@ export const OpenModeSchemaItems: React.FC<Options> = (options) => {
<div style={{ alignItems: 'center', display: 'flex', justifyContent: 'space-between' }}>
{t('Popup size')}
<Select
data-testid="antd-select"
bordered={false}
options={[
{ label: t('Small'), value: 'small' },

View File

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

View File

@ -3,7 +3,8 @@ import { css } from '@emotion/css';
import { useField, useFieldSchema } from '@formily/react';
import { Space } from 'antd';
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 { DragHandler, useCompile, useDesignable, useGridContext, useGridRowContext } from '../schema-component';
import { gridRowColWrap } from '../schema-initializer/utils';
@ -35,7 +36,72 @@ const overrideAntdCSS = css`
& .ant-space-item .anticon {
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) => {
const { disableInitializer, title, template, draggable = true } = props;
const { dn, designable } = useDesignable();
@ -43,12 +109,15 @@ export const GeneralSchemaDesigner = (props: any) => {
const { t } = useTranslation();
const fieldSchema = useFieldSchema();
const compile = useCompile();
const [visible, setVisible] = useState(false);
const schemaSettingsProps = {
dn,
field,
fieldSchema,
};
const onShow = useCallback((value) => setVisible(value), []);
const rowCtx = useGridRowContext();
const ctx = useGridContext();
const templateName = ['FormItem', 'ReadPrettyFormItem'].includes(template?.componentName)
@ -58,9 +127,7 @@ export const GeneralSchemaDesigner = (props: any) => {
return {
insertPosition: 'afterEnd',
wrap: rowCtx?.cols?.length > 1 ? undefined : gridRowColWrap,
component: (
<PlusOutlined data-testid="GeneralSchemaDesigner-Initializer" style={{ cursor: 'pointer', fontSize: 14 }} />
),
component: <PlusOutlined data-testid="designer-add-block" style={{ cursor: 'pointer', fontSize: 14 }} />,
};
}, [rowCtx?.cols?.length]);
@ -69,45 +136,46 @@ export const GeneralSchemaDesigner = (props: any) => {
}
return (
<div className={classNames('general-schema-designer', overrideAntdCSS)}>
{title && (
<div className={classNames('general-schema-designer-title', titleCss)}>
<Space size={2}>
<span className={'title-tag'}>{compile(title)}</span>
{template && (
<span className={'title-tag'}>
{t('Reference template')}: {templateName || t('Untitled')}
</span>
)}
</Space>
</div>
)}
<div className={'general-schema-designer-icons'}>
<Space size={2} align={'center'}>
{draggable && (
<DragHandler>
<DragOutlined data-testid="GeneralSchemaDesigner-DragHandler" />
</DragHandler>
<DesignerControl onShow={onShow}>
{visible ? (
<>
{title && (
<div className={classNames('general-schema-designer-title', titleCss)}>
<Space size={2}>
<span className={'title-tag'}>{compile(title)}</span>
{template && (
<span className={'title-tag'}>
{t('Reference template')}: {templateName || t('Untitled')}
</span>
)}
</Space>
</div>
)}
{!disableInitializer &&
(ctx?.InitializerComponent ? (
<ctx.InitializerComponent {...initializerProps} />
) : (
ctx?.renderSchemaInitializer?.(initializerProps)
))}
<SchemaSettings
title={
<MenuOutlined
data-testid="GeneralSchemaDesigner-SchemaSettings"
style={{ cursor: 'pointer', fontSize: 12 }}
/>
}
{...schemaSettingsProps}
>
{props.children}
</SchemaSettings>
</Space>
</div>
</div>
<div className={'general-schema-designer-icons'}>
<Space size={2} align={'center'}>
{draggable && (
<DragHandler>
<DragOutlined data-testid="designer-drag" />
</DragHandler>
)}
{!disableInitializer &&
(ctx?.InitializerComponent ? (
<ctx.InitializerComponent {...initializerProps} />
) : (
ctx?.renderSchemaInitializer?.(initializerProps)
))}
<SchemaSettings
title={
<MenuOutlined data-testid="designer-schema-settings" style={{ cursor: 'pointer', fontSize: 12 }} />
}
{...schemaSettingsProps}
>
{props.children}
</SchemaSettings>
</Space>
</div>
</>
) : null}
</DesignerControl>
);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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