@nocobase/client-v2 2.3.0-alpha.1 → 2.3.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/es/APIClient.d.ts +12 -0
- package/es/components/form/ScanInput/useCodeScanner.d.ts +12 -2
- package/es/components/form/ScanInput/zxingWasmDecoder.d.ts +9 -0
- package/es/flow/actions/linkageRules.d.ts +25 -0
- package/es/flow/components/FieldAssignValueInput.d.ts +6 -30
- package/es/flow/components/FlowRoute.d.ts +2 -2
- package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +24 -0
- package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +32 -0
- package/es/flow/components/field-value-variable/dateValue.d.ts +36 -0
- package/es/flow/components/field-value-variable/index.d.ts +11 -0
- package/es/flow/components/filter/VariableFilterItem.d.ts +7 -0
- package/es/flow/components/placeholders/BlockPlaceholder.d.ts +1 -0
- package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
- package/es/flow/internal/utils/operatorSchemaHelper.d.ts +2 -2
- package/es/flow/models/actions/UpdateRecordActionUtils.d.ts +8 -2
- package/es/flow/models/base/ActionModelCore.d.ts +4 -2
- package/es/flow/models/base/BlockGridModel.d.ts +3 -0
- package/es/flow/models/base/CollectionBlockModel.d.ts +3 -0
- package/es/flow/models/blocks/details/DetailsBlockModel.d.ts +3 -0
- package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +1 -0
- package/es/flow/models/blocks/filter-form/FilterFormSubmitActionModel.d.ts +3 -1
- package/es/flow/models/blocks/form/FormBlockModel.d.ts +7 -0
- package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
- package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
- package/es/flow/models/blocks/table/JSColumnModel.d.ts +2 -0
- package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
- package/es/flow/models/blocks/table/TableColumnModel.d.ts +12 -0
- package/es/flow/models/blocks/table/utils.d.ts +1 -0
- package/es/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.d.ts +2 -1
- package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +0 -17
- package/es/flow/models/fields/AssociationFieldModel/recordSelectShared.d.ts +41 -0
- package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
- package/es/flow-compat/passwordUtils.d.ts +1 -1
- package/es/index.d.ts +1 -0
- package/es/index.mjs +148 -145
- package/es/ui-operation/index.d.ts +15 -0
- package/es/ui-operation/ui-operation-codec.d.ts +10 -0
- package/lib/index.js +170 -167
- package/lib/locale/languageCodes.js +1 -0
- package/package.json +10 -8
- package/src/APIClient.ts +92 -0
- package/src/Application.tsx +3 -1
- package/src/__tests__/APIClient.test.tsx +58 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +7 -2
- package/src/__tests__/settings-center.test.tsx +270 -6
- package/src/acl/ACLProvider.tsx +2 -0
- package/src/acl/__tests__/ACLProvider.test.tsx +92 -0
- package/src/collection-manager/__tests__/field-configure.test.ts +52 -0
- package/src/collection-manager/field-configure.ts +2 -2
- package/src/components/form/ScanInput/CodeScanner.tsx +64 -35
- package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +107 -0
- package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +294 -24
- package/src/components/form/ScanInput/__tests__/zxingWasmDecoder.test.ts +78 -0
- package/src/components/form/ScanInput/useCodeScanner.ts +281 -23
- package/src/components/form/ScanInput/zxingWasmDecoder.ts +64 -0
- package/src/components/form/filter/CollectionFilterItem.tsx +4 -2
- package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +17 -0
- package/src/components/form/table/Table.tsx +81 -6
- package/src/components/form/table/__tests__/Table.columnWidth.test.tsx +433 -0
- package/src/flow/__tests__/FlowRoute.test.tsx +126 -8
- package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
- package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
- package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
- package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +72 -0
- package/src/flow/actions/linkageRules.tsx +59 -16
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +78 -59
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +228 -0
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +42 -23
- package/src/flow/common/Markdown/style.ts +4 -0
- package/src/flow/components/BlockItemCard.tsx +2 -2
- package/src/flow/components/FieldAssignValueInput.tsx +42 -621
- package/src/flow/components/FlowRoute.tsx +71 -29
- package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
- package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +335 -0
- package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +389 -0
- package/src/flow/components/field-value-variable/dateValue.ts +223 -0
- package/src/flow/components/field-value-variable/index.ts +12 -0
- package/src/flow/components/filter/VariableFilterItem.tsx +23 -6
- package/src/flow/components/filter/__tests__/VariableFilterItem.rightMetaTree.test.tsx +158 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +125 -0
- package/src/flow/components/placeholders/BlockPlaceholder.tsx +70 -23
- package/src/flow/components/placeholders/__tests__/BlockPlaceholder.test.tsx +57 -7
- package/src/flow/index.ts +2 -0
- package/src/flow/internal/components/Markdown/DisplayMarkdown.tsx +14 -9
- package/src/flow/internal/components/Markdown/__tests__/DisplayMarkdown.test.tsx +46 -0
- package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
- package/src/flow/internal/utils/operatorSchemaHelper.ts +3 -2
- package/src/flow/models/actions/UpdateRecordActionModel.tsx +18 -1
- package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
- package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
- package/src/flow/models/base/ActionModelCore.tsx +11 -2
- package/src/flow/models/base/BlockGridModel.tsx +26 -0
- package/src/flow/models/base/CollectionBlockModel.tsx +38 -3
- package/src/flow/models/base/GridModel.tsx +0 -1
- package/src/flow/models/base/PageModel/PageTabModel.tsx +67 -17
- package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +509 -12
- package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
- package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
- package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
- package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +125 -9
- package/src/flow/models/blocks/assign-form/AssignFormGridModel.tsx +22 -1
- package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +36 -50
- package/src/flow/models/blocks/assign-form/__tests__/assignFieldValuesFlow.editor.test.tsx +140 -0
- package/src/flow/models/blocks/details/DetailsBlockModel.tsx +8 -3
- package/src/flow/models/blocks/details/__tests__/DetailsBlockModel.initialPaginationChangeRefresh.test.ts +32 -1
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +27 -3
- package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +36 -5
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +129 -14
- package/src/flow/models/blocks/filter-form/FilterFormSubmitActionModel.tsx +57 -4
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.onModelCreated.test.ts +174 -1
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.toggleFormFieldsCollapse.test.ts +29 -0
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +359 -1
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +72 -0
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormSubmitActionModel.lifecycle.test.ts +90 -0
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +32 -4
- package/src/flow/models/blocks/filter-form/fields/FieldComponentProps.tsx +1 -1
- package/src/flow/models/blocks/filter-form/fields/__tests__/FieldComponentProps.options.test.tsx +61 -0
- package/src/flow/models/blocks/filter-manager/FilterManager.ts +5 -0
- package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +40 -0
- package/src/flow/models/blocks/form/FormBlockModel.tsx +73 -5
- package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +153 -2
- package/src/flow/models/blocks/form/__tests__/popupLinkage.test.tsx +175 -0
- package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -0
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
- package/src/flow/models/blocks/form/value-runtime/rules.ts +67 -14
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
- package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
- package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +28 -2
- package/src/flow/models/blocks/table/TableColumnModel.tsx +48 -9
- package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.filterReset.test.ts +78 -0
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
- package/src/flow/models/blocks/table/utils.ts +7 -0
- package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +33 -1
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +8 -0
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
- package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +29 -3
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +9 -136
- package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +191 -3
- package/src/flow/models/fields/AssociationFieldModel/recordSelectShared.tsx +156 -0
- package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
- package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
- package/src/flow/models/fields/DisplayAssociationField/DisplaySubTableFieldModel.tsx +42 -7
- package/src/flow/models/fields/DisplayAssociationField/__tests__/DisplaySubTableFieldModel.test.tsx +351 -0
- package/src/flow/models/fields/DisplayHtmlFieldModel.tsx +2 -1
- package/src/flow/models/fields/DisplayNumberFieldModel.tsx +1 -1
- package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
- package/src/flow/models/fields/NumberFieldModel.tsx +1 -1
- package/src/flow/models/fields/RichTextFieldModel/__tests__/RichTextFieldModel.test.tsx +74 -0
- package/src/flow/models/fields/RichTextFieldModel/index.tsx +27 -6
- package/src/flow/models/fields/__tests__/DisplayNumberFieldModel.test.ts +33 -0
- package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
- package/src/flow/models/fields/__tests__/NumberFieldModel.test.tsx +47 -0
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +6 -0
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +27 -2
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +139 -13
- package/src/flow/models/topbar/TopbarActionModel.tsx +5 -5
- package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
- package/src/index.ts +1 -0
- package/src/layout-manager/LayoutContentRoute.tsx +2 -1
- package/src/layout-manager/LayoutRoute.tsx +2 -1
- package/src/layout-manager/__tests__/LayoutRoute.test.tsx +87 -1
- package/src/locale/languageCodes.ts +1 -0
- package/src/settings-center/AdminSettingsLayout.tsx +23 -2
- package/src/ui-operation/index.ts +33 -0
- package/src/ui-operation/ui-operation-codec.ts +54 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { fireEvent, render } from '@testing-library/react';
|
|
11
|
+
import { css } from '@emotion/css';
|
|
12
|
+
import { ConfigProvider, Table as AntdTable } from 'antd';
|
|
13
|
+
import type { ColumnsType } from 'antd/es/table/interface';
|
|
14
|
+
import type { RenderedCell } from 'rc-table/lib/interface';
|
|
15
|
+
import React from 'react';
|
|
16
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
17
|
+
import { Table, type TableProps } from '../Table';
|
|
18
|
+
|
|
19
|
+
type Row = {
|
|
20
|
+
id: number;
|
|
21
|
+
name: string;
|
|
22
|
+
note: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const row: Row = {
|
|
26
|
+
id: 1,
|
|
27
|
+
name: 'Long unbroken content',
|
|
28
|
+
note: 'Secondary content',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const defaultColumnMaxWidth = 400;
|
|
32
|
+
const defaultColumnContentClassName = 'nb-table-default-column-content';
|
|
33
|
+
|
|
34
|
+
function getMatchingStyleValues(element: Element, property: string) {
|
|
35
|
+
return Array.from(document.styleSheets).flatMap((styleSheet) => {
|
|
36
|
+
try {
|
|
37
|
+
return Array.from(styleSheet.cssRules).flatMap((rule) => {
|
|
38
|
+
if (!(rule instanceof CSSStyleRule)) return [];
|
|
39
|
+
try {
|
|
40
|
+
return element.matches(rule.selectorText) ? [rule.style.getPropertyValue(property)] : [];
|
|
41
|
+
} catch {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
} catch {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function renderTable(columns: ColumnsType<Row>, props: Partial<TableProps<Row>> = {}) {
|
|
52
|
+
return render(
|
|
53
|
+
<ConfigProvider
|
|
54
|
+
theme={{
|
|
55
|
+
token: {
|
|
56
|
+
screenXS: 500,
|
|
57
|
+
paddingXL: 40,
|
|
58
|
+
padding: 10,
|
|
59
|
+
},
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
<Table<Row> rowKey="id" columns={columns} dataSource={[row]} pagination={false} {...props} />
|
|
63
|
+
</ConfigProvider>,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getResolvedMaxWidth(element: HTMLElement) {
|
|
68
|
+
const style = window.getComputedStyle(element);
|
|
69
|
+
const variableName = style.maxWidth.match(/^var\((--[^)]+)\)$/)?.[1];
|
|
70
|
+
return variableName ? style.getPropertyValue(variableName).trim() : style.maxWidth;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function expectDefaultWrappingStyle(element: Element | null) {
|
|
74
|
+
expect(element).not.toBeNull();
|
|
75
|
+
const htmlElement = element as HTMLElement;
|
|
76
|
+
const style = window.getComputedStyle(htmlElement);
|
|
77
|
+
expect(htmlElement.classList).toContain(defaultColumnContentClassName);
|
|
78
|
+
expect(getResolvedMaxWidth(htmlElement)).toBe(`${defaultColumnMaxWidth}px`);
|
|
79
|
+
expect(style.whiteSpace).toBe('normal');
|
|
80
|
+
expect(style.overflowWrap).toBe('break-word');
|
|
81
|
+
expect(style.wordBreak).toBe('break-word');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function expectNoDefaultWrappingStyle(element: Element | null) {
|
|
85
|
+
expect(element).not.toBeNull();
|
|
86
|
+
const htmlElement = element as HTMLElement;
|
|
87
|
+
expect(htmlElement.classList).not.toContain(defaultColumnContentClassName);
|
|
88
|
+
expect(getResolvedMaxWidth(htmlElement)).not.toBe(`${defaultColumnMaxWidth}px`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
describe('Table default column content width', () => {
|
|
92
|
+
it('adds token-derived wrapping styles to unconstrained leaf headers and cells', () => {
|
|
93
|
+
const { getByRole } = renderTable([{ title: 'Name', dataIndex: 'name' }]);
|
|
94
|
+
|
|
95
|
+
expectDefaultWrappingStyle(getByRole('columnheader', { name: 'Name' }));
|
|
96
|
+
expectDefaultWrappingStyle(getByRole('cell', { name: row.name }));
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('leaves columns with explicit width, ellipsis, or fixed positioning unchanged', () => {
|
|
100
|
+
const columns: ColumnsType<Row> = [
|
|
101
|
+
{ title: 'Width', dataIndex: 'name', width: 240 },
|
|
102
|
+
{ title: 'Ellipsis', dataIndex: 'note', ellipsis: true },
|
|
103
|
+
{ title: 'Fixed', dataIndex: 'id', fixed: 'left' },
|
|
104
|
+
];
|
|
105
|
+
const { getByRole } = renderTable(columns, { scroll: { x: 800 } });
|
|
106
|
+
|
|
107
|
+
for (const title of ['Width', 'Ellipsis', 'Fixed']) {
|
|
108
|
+
expectNoDefaultWrappingStyle(getByRole('columnheader', { name: title }));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const widthCell = getByRole('cell', { name: row.name });
|
|
112
|
+
const ellipsisCell = getByRole('cell', { name: row.note });
|
|
113
|
+
const fixedCell = getByRole('cell', { name: String(row.id) });
|
|
114
|
+
for (const cell of [widthCell, ellipsisCell, fixedCell]) {
|
|
115
|
+
expectNoDefaultWrappingStyle(cell);
|
|
116
|
+
}
|
|
117
|
+
expect(ellipsisCell.classList).toContain('ant-table-cell-ellipsis');
|
|
118
|
+
expect(window.getComputedStyle(ellipsisCell).whiteSpace).toBe('nowrap');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('preserves cell callbacks and lets caller styles override the defaults', () => {
|
|
122
|
+
const handleClick = vi.fn();
|
|
123
|
+
const onCell = vi.fn(() => ({
|
|
124
|
+
colSpan: 2,
|
|
125
|
+
className: 'custom-body-cell',
|
|
126
|
+
onClick: handleClick,
|
|
127
|
+
style: {
|
|
128
|
+
maxWidth: 120,
|
|
129
|
+
whiteSpace: 'pre' as const,
|
|
130
|
+
},
|
|
131
|
+
}));
|
|
132
|
+
const onHeaderCell = vi.fn(() => ({
|
|
133
|
+
colSpan: 2,
|
|
134
|
+
className: 'custom-header-cell',
|
|
135
|
+
style: {
|
|
136
|
+
maxWidth: 180,
|
|
137
|
+
overflowWrap: 'normal' as const,
|
|
138
|
+
},
|
|
139
|
+
}));
|
|
140
|
+
const columns: ColumnsType<Row> = [
|
|
141
|
+
{ title: 'Name', dataIndex: 'name', onCell, onHeaderCell },
|
|
142
|
+
{ title: 'Note', dataIndex: 'note', width: 100 },
|
|
143
|
+
];
|
|
144
|
+
const { container } = renderTable(columns);
|
|
145
|
+
|
|
146
|
+
const header = container.querySelector<HTMLElement>('th.custom-header-cell');
|
|
147
|
+
expect(header?.getAttribute('colspan')).toBe('2');
|
|
148
|
+
const headerStyle = window.getComputedStyle(header as HTMLElement);
|
|
149
|
+
expect(headerStyle.maxWidth).toBe('180px');
|
|
150
|
+
expect(headerStyle.whiteSpace).toBe('normal');
|
|
151
|
+
expect(headerStyle.overflowWrap).toBe('normal');
|
|
152
|
+
expect(headerStyle.wordBreak).toBe('break-word');
|
|
153
|
+
|
|
154
|
+
const cell = container.querySelector<HTMLElement>('td.custom-body-cell');
|
|
155
|
+
expect(cell?.getAttribute('colspan')).toBe('2');
|
|
156
|
+
const cellStyle = window.getComputedStyle(cell as HTMLElement);
|
|
157
|
+
expect(cellStyle.maxWidth).toBe('120px');
|
|
158
|
+
expect(cellStyle.whiteSpace).toBe('pre');
|
|
159
|
+
expect(cellStyle.overflowWrap).toBe('break-word');
|
|
160
|
+
expect(cellStyle.wordBreak).toBe('break-word');
|
|
161
|
+
|
|
162
|
+
fireEvent.click(cell as HTMLElement);
|
|
163
|
+
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
164
|
+
expect(onCell).toHaveBeenCalledWith(row, 0);
|
|
165
|
+
expect(onHeaderCell).toHaveBeenCalledWith(expect.objectContaining({ title: 'Name' }));
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('keeps the default wrapping behavior when caller classNames have no styles', () => {
|
|
169
|
+
const columns: ColumnsType<Row> = [
|
|
170
|
+
{
|
|
171
|
+
title: 'Name',
|
|
172
|
+
dataIndex: 'name',
|
|
173
|
+
className: 'column-marker',
|
|
174
|
+
onCell: () => ({ className: 'body-marker' }),
|
|
175
|
+
onHeaderCell: () => ({ className: 'header-marker' }),
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
title: 'Note',
|
|
179
|
+
dataIndex: 'note',
|
|
180
|
+
render: () => ({ children: 'Rendered marker', props: { className: 'render-marker' } }),
|
|
181
|
+
},
|
|
182
|
+
];
|
|
183
|
+
const { getByRole } = renderTable(columns);
|
|
184
|
+
|
|
185
|
+
const nameHeader = getByRole('columnheader', { name: 'Name' });
|
|
186
|
+
const nameCell = getByRole('cell', { name: row.name });
|
|
187
|
+
const renderedCell = getByRole('cell', { name: 'Rendered marker' });
|
|
188
|
+
expect(nameHeader.classList).toContain('column-marker');
|
|
189
|
+
expect(nameHeader.classList).toContain('header-marker');
|
|
190
|
+
expect(nameCell.classList).toContain('column-marker');
|
|
191
|
+
expect(nameCell.classList).toContain('body-marker');
|
|
192
|
+
expect(renderedCell.classList).toContain('render-marker');
|
|
193
|
+
for (const element of [nameHeader, nameCell, renderedCell]) {
|
|
194
|
+
expectDefaultWrappingStyle(element);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('lets caller className styles override the defaults', () => {
|
|
199
|
+
const callerClassName = css`
|
|
200
|
+
max-width: 135px;
|
|
201
|
+
white-space: pre;
|
|
202
|
+
overflow-wrap: normal;
|
|
203
|
+
word-break: normal;
|
|
204
|
+
`;
|
|
205
|
+
const columns: ColumnsType<Row> = [
|
|
206
|
+
{
|
|
207
|
+
title: 'Name',
|
|
208
|
+
dataIndex: 'name',
|
|
209
|
+
onCell: () => ({ className: callerClassName }),
|
|
210
|
+
onHeaderCell: () => ({ className: callerClassName }),
|
|
211
|
+
},
|
|
212
|
+
{ title: 'Note', dataIndex: 'note', className: callerClassName },
|
|
213
|
+
];
|
|
214
|
+
// Use a distinct token value so this render creates its default Emotion class after the caller class. This
|
|
215
|
+
// catches source-order regressions that would be hidden if a previous test had already inserted the default rule.
|
|
216
|
+
const { getByRole } = render(
|
|
217
|
+
<ConfigProvider theme={{ token: { screenXS: 501, paddingXL: 40, padding: 10 } }}>
|
|
218
|
+
<Table<Row> rowKey="id" columns={columns} dataSource={[row]} pagination={false} />
|
|
219
|
+
</ConfigProvider>,
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
for (const element of [
|
|
223
|
+
getByRole('columnheader', { name: 'Name' }),
|
|
224
|
+
getByRole('columnheader', { name: 'Note' }),
|
|
225
|
+
getByRole('cell', { name: row.name }),
|
|
226
|
+
getByRole('cell', { name: row.note }),
|
|
227
|
+
]) {
|
|
228
|
+
const style = window.getComputedStyle(element);
|
|
229
|
+
expect(getResolvedMaxWidth(element)).toBe('135px');
|
|
230
|
+
expect(style.whiteSpace).toBe('pre');
|
|
231
|
+
expect(style.overflowWrap).not.toBe('anywhere');
|
|
232
|
+
expect(getMatchingStyleValues(element, 'overflow-wrap')).toContain('normal');
|
|
233
|
+
expect(style.wordBreak).toBe('normal');
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('lets className returned from render override the defaults on first mount', () => {
|
|
238
|
+
const callerClassName = css`
|
|
239
|
+
max-width: 136px;
|
|
240
|
+
white-space: pre;
|
|
241
|
+
overflow-wrap: normal;
|
|
242
|
+
word-break: normal;
|
|
243
|
+
`;
|
|
244
|
+
const renderedCell: RenderedCell<Row> = {
|
|
245
|
+
props: { className: callerClassName },
|
|
246
|
+
};
|
|
247
|
+
const { container } = render(
|
|
248
|
+
<ConfigProvider theme={{ token: { screenXS: 502, paddingXL: 40, padding: 10 } }}>
|
|
249
|
+
<Table<Row>
|
|
250
|
+
rowKey="id"
|
|
251
|
+
columns={[{ title: 'Name', dataIndex: 'name', render: () => renderedCell }]}
|
|
252
|
+
dataSource={[row]}
|
|
253
|
+
pagination={false}
|
|
254
|
+
/>
|
|
255
|
+
</ConfigProvider>,
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const cell = container.querySelector<HTMLElement>('.ant-table-tbody td');
|
|
259
|
+
expect(cell).not.toBeNull();
|
|
260
|
+
const htmlCell = cell as HTMLElement;
|
|
261
|
+
const style = window.getComputedStyle(htmlCell);
|
|
262
|
+
expect(getResolvedMaxWidth(htmlCell)).toBe('136px');
|
|
263
|
+
expect(style.whiteSpace).toBe('pre');
|
|
264
|
+
expect(style.overflowWrap).not.toBe('anywhere');
|
|
265
|
+
expect(getMatchingStyleValues(cell as HTMLElement, 'overflow-wrap')).toContain('normal');
|
|
266
|
+
expect(style.wordBreak).toBe('normal');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('lets static caller className styles override defaults inserted later', () => {
|
|
270
|
+
const callerClassName = 'caller-static-table-column';
|
|
271
|
+
const callerStyle = document.createElement('style');
|
|
272
|
+
callerStyle.textContent = `
|
|
273
|
+
.${callerClassName} {
|
|
274
|
+
max-width: 137px;
|
|
275
|
+
white-space: pre;
|
|
276
|
+
overflow-wrap: normal;
|
|
277
|
+
word-break: normal;
|
|
278
|
+
}
|
|
279
|
+
`;
|
|
280
|
+
document.head.prepend(callerStyle);
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
const columns: ColumnsType<Row> = [
|
|
284
|
+
{
|
|
285
|
+
title: 'Name',
|
|
286
|
+
dataIndex: 'name',
|
|
287
|
+
onCell: () => ({ className: callerClassName }),
|
|
288
|
+
onHeaderCell: () => ({ className: callerClassName }),
|
|
289
|
+
},
|
|
290
|
+
{ title: 'Note', dataIndex: 'note', className: callerClassName },
|
|
291
|
+
{
|
|
292
|
+
title: 'Rendered',
|
|
293
|
+
dataIndex: 'id',
|
|
294
|
+
render: () => ({ props: { className: callerClassName } }),
|
|
295
|
+
},
|
|
296
|
+
];
|
|
297
|
+
const { container, getByRole } = render(
|
|
298
|
+
<ConfigProvider theme={{ token: { screenXS: 503, paddingXL: 40, padding: 10 } }}>
|
|
299
|
+
<Table<Row> rowKey="id" columns={columns} dataSource={[row]} pagination={false} />
|
|
300
|
+
</ConfigProvider>,
|
|
301
|
+
);
|
|
302
|
+
const cells = Array.from(container.querySelectorAll<HTMLElement>('.ant-table-tbody td'));
|
|
303
|
+
|
|
304
|
+
for (const element of [
|
|
305
|
+
getByRole('columnheader', { name: 'Name' }),
|
|
306
|
+
getByRole('columnheader', { name: 'Note' }),
|
|
307
|
+
cells[0],
|
|
308
|
+
cells[1],
|
|
309
|
+
cells[2],
|
|
310
|
+
]) {
|
|
311
|
+
expect(element).toBeDefined();
|
|
312
|
+
const htmlElement = element as HTMLElement;
|
|
313
|
+
const style = window.getComputedStyle(htmlElement);
|
|
314
|
+
expect(getResolvedMaxWidth(htmlElement)).toBe('137px');
|
|
315
|
+
expect(style.whiteSpace).toBe('pre');
|
|
316
|
+
expect(style.overflowWrap).not.toBe('anywhere');
|
|
317
|
+
expect(getMatchingStyleValues(htmlElement, 'overflow-wrap')).toContain('normal');
|
|
318
|
+
expect(style.wordBreak).toBe('normal');
|
|
319
|
+
}
|
|
320
|
+
} finally {
|
|
321
|
+
callerStyle.remove();
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('lets styles returned from render override the defaults', () => {
|
|
326
|
+
const renderedCell: RenderedCell<Row> = {
|
|
327
|
+
children: 'Rendered cell',
|
|
328
|
+
props: {
|
|
329
|
+
className: 'rendered-cell',
|
|
330
|
+
style: {
|
|
331
|
+
maxWidth: 123,
|
|
332
|
+
whiteSpace: 'pre',
|
|
333
|
+
overflowWrap: 'normal',
|
|
334
|
+
wordBreak: 'normal',
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
const { getByRole } = renderTable([{ title: 'Name', dataIndex: 'name', render: () => renderedCell }]);
|
|
339
|
+
|
|
340
|
+
const style = window.getComputedStyle(getByRole('cell', { name: 'Rendered cell' }));
|
|
341
|
+
expect(style.maxWidth).toBe('123px');
|
|
342
|
+
expect(style.whiteSpace).toBe('pre');
|
|
343
|
+
expect(style.overflowWrap).toBe('normal');
|
|
344
|
+
expect(style.wordBreak).toBe('normal');
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('processes only leaves in nested columns without mutating the input', () => {
|
|
348
|
+
const nameColumn = { title: 'Name', dataIndex: 'name' as const };
|
|
349
|
+
const noteColumn = { title: 'Note', dataIndex: 'note' as const };
|
|
350
|
+
const children = [nameColumn, noteColumn];
|
|
351
|
+
const groupColumn = { title: 'Details', children };
|
|
352
|
+
const columns: ColumnsType<Row> = [groupColumn];
|
|
353
|
+
|
|
354
|
+
const { getByRole } = renderTable(columns);
|
|
355
|
+
|
|
356
|
+
expectNoDefaultWrappingStyle(getByRole('columnheader', { name: 'Details' }));
|
|
357
|
+
expectDefaultWrappingStyle(getByRole('columnheader', { name: 'Name' }));
|
|
358
|
+
expectDefaultWrappingStyle(getByRole('columnheader', { name: 'Note' }));
|
|
359
|
+
expectDefaultWrappingStyle(getByRole('cell', { name: row.name }));
|
|
360
|
+
expectDefaultWrappingStyle(getByRole('cell', { name: row.note }));
|
|
361
|
+
|
|
362
|
+
expect(columns[0]).toBe(groupColumn);
|
|
363
|
+
expect(groupColumn.children).toBe(children);
|
|
364
|
+
expect(children[0]).toBe(nameColumn);
|
|
365
|
+
expect(children[1]).toBe(noteColumn);
|
|
366
|
+
expect(nameColumn).not.toHaveProperty('onCell');
|
|
367
|
+
expect(nameColumn).not.toHaveProperty('onHeaderCell');
|
|
368
|
+
expect(noteColumn).not.toHaveProperty('onCell');
|
|
369
|
+
expect(noteColumn).not.toHaveProperty('onHeaderCell');
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('does not apply the default boundary to selection or drag-handle columns', () => {
|
|
373
|
+
const columns: ColumnsType<Row> = [{ title: 'Name', dataIndex: 'name' }];
|
|
374
|
+
const selectionTable = renderTable(columns, { rowSelection: {} });
|
|
375
|
+
|
|
376
|
+
expectNoDefaultWrappingStyle(selectionTable.container.querySelector<HTMLElement>('th.ant-table-selection-column'));
|
|
377
|
+
expectNoDefaultWrappingStyle(selectionTable.container.querySelector<HTMLElement>('td.ant-table-selection-column'));
|
|
378
|
+
expectDefaultWrappingStyle(selectionTable.getByRole('columnheader', { name: 'Name' }));
|
|
379
|
+
selectionTable.unmount();
|
|
380
|
+
|
|
381
|
+
const dragTable = renderTable(columns, { isDraggable: true, onSortEnd: vi.fn() });
|
|
382
|
+
const dragHeader = dragTable.container.querySelector<HTMLElement>('.ant-table-thead th:first-child');
|
|
383
|
+
const dragCell = dragTable.container.querySelector<HTMLElement>('.ant-table-tbody td:first-child');
|
|
384
|
+
|
|
385
|
+
expectNoDefaultWrappingStyle(dragHeader);
|
|
386
|
+
expectNoDefaultWrappingStyle(dragCell);
|
|
387
|
+
expectDefaultWrappingStyle(dragTable.getByRole('columnheader', { name: 'Name' }));
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('preserves Ant Design sentinels used to position expand and selection columns', () => {
|
|
391
|
+
const columns: ColumnsType<Row> = [
|
|
392
|
+
{ title: 'Name', dataIndex: 'name' },
|
|
393
|
+
AntdTable.EXPAND_COLUMN,
|
|
394
|
+
AntdTable.SELECTION_COLUMN,
|
|
395
|
+
{ title: 'Note', dataIndex: 'note' },
|
|
396
|
+
];
|
|
397
|
+
const { container } = renderTable(columns, {
|
|
398
|
+
expandable: { expandedRowRender: (record) => <span>{record.note}</span> },
|
|
399
|
+
rowSelection: {},
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
const headers = Array.from(container.querySelectorAll<HTMLElement>('.ant-table-thead th'));
|
|
403
|
+
expect(headers).toHaveLength(4);
|
|
404
|
+
expect(headers[0].textContent).toBe('Name');
|
|
405
|
+
expect(headers[1].classList).toContain('ant-table-row-expand-icon-cell');
|
|
406
|
+
expect(headers[2].classList).toContain('ant-table-selection-column');
|
|
407
|
+
expect(headers[3].textContent).toBe('Note');
|
|
408
|
+
expectNoDefaultWrappingStyle(headers[1]);
|
|
409
|
+
expectNoDefaultWrappingStyle(headers[2]);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('treats empty, undefined, or null children as leaves like antd', () => {
|
|
413
|
+
const columns = [
|
|
414
|
+
{ title: 'Empty children', dataIndex: 'name', children: [] },
|
|
415
|
+
{ title: 'Undefined children', dataIndex: 'note', children: undefined },
|
|
416
|
+
{ title: 'Null children', dataIndex: 'id', children: null },
|
|
417
|
+
] as unknown as ColumnsType<Row>;
|
|
418
|
+
const { getByRole } = renderTable(columns);
|
|
419
|
+
|
|
420
|
+
expectDefaultWrappingStyle(getByRole('columnheader', { name: 'Empty children' }));
|
|
421
|
+
expectDefaultWrappingStyle(getByRole('columnheader', { name: 'Undefined children' }));
|
|
422
|
+
expectDefaultWrappingStyle(getByRole('columnheader', { name: 'Null children' }));
|
|
423
|
+
expectDefaultWrappingStyle(getByRole('cell', { name: row.name }));
|
|
424
|
+
expectDefaultWrappingStyle(getByRole('cell', { name: row.note }));
|
|
425
|
+
expectDefaultWrappingStyle(getByRole('cell', { name: String(row.id) }));
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it('does not force fixed table layout', () => {
|
|
429
|
+
const { container } = renderTable([{ title: 'Name', dataIndex: 'name' }]);
|
|
430
|
+
|
|
431
|
+
expect(container.querySelector<HTMLTableElement>('table')?.style.tableLayout).not.toBe('fixed');
|
|
432
|
+
});
|
|
433
|
+
});
|
|
@@ -116,6 +116,7 @@ describe('FlowRoute', () => {
|
|
|
116
116
|
}),
|
|
117
117
|
);
|
|
118
118
|
});
|
|
119
|
+
expect(engine.context.deviceType).toBe('computer');
|
|
119
120
|
|
|
120
121
|
rerender(
|
|
121
122
|
<FlowEngineProvider engine={engine}>
|
|
@@ -581,14 +582,14 @@ describe('FlowRoute', () => {
|
|
|
581
582
|
expect(getRouteBySchemaUid).toHaveBeenCalledTimes(1);
|
|
582
583
|
});
|
|
583
584
|
|
|
584
|
-
it('should
|
|
585
|
+
it('should explain how to open a legacy page outside the v2 runtime', async () => {
|
|
585
586
|
const originalLocation = window.location;
|
|
586
587
|
const replace = vi.fn();
|
|
587
588
|
Object.defineProperty(window, 'location', {
|
|
588
589
|
configurable: true,
|
|
589
590
|
value: {
|
|
590
591
|
...originalLocation,
|
|
591
|
-
pathname: '/v2/admin/test-page/tab/tab-1',
|
|
592
|
+
pathname: '/nocobase/v2/apps/jhb20/admin/test-page/tab/tab-1',
|
|
592
593
|
search: '?from=direct',
|
|
593
594
|
hash: '#dialog',
|
|
594
595
|
replace,
|
|
@@ -607,9 +608,9 @@ describe('FlowRoute', () => {
|
|
|
607
608
|
});
|
|
608
609
|
engine.context.defineProperty('app', {
|
|
609
610
|
value: {
|
|
610
|
-
getPublicPath: () => '/v2/',
|
|
611
|
+
getPublicPath: () => '/nocobase/v2/',
|
|
611
612
|
router: {
|
|
612
|
-
getBasename: () => '/v2',
|
|
613
|
+
getBasename: () => '/nocobase/v2',
|
|
613
614
|
},
|
|
614
615
|
},
|
|
615
616
|
});
|
|
@@ -625,15 +626,25 @@ describe('FlowRoute', () => {
|
|
|
625
626
|
|
|
626
627
|
render(
|
|
627
628
|
<FlowEngineProvider engine={engine}>
|
|
628
|
-
<MemoryRouter initialEntries={['/
|
|
629
|
+
<MemoryRouter initialEntries={['/nocobase/v2/apps/jhb20/admin/test-page/tab/tab-1?from=direct#dialog']}>
|
|
629
630
|
<Routes>
|
|
630
|
-
<Route path="/
|
|
631
|
+
<Route path="/nocobase/v2/apps/jhb20/admin/:name/*" element={<FlowRoute />} />
|
|
631
632
|
</Routes>
|
|
632
633
|
</MemoryRouter>
|
|
633
634
|
</FlowEngineProvider>,
|
|
634
635
|
);
|
|
635
636
|
|
|
636
|
-
expect(await screen.findByText('
|
|
637
|
+
expect(await screen.findByText('This page is not supported in the /v2/ branch')).toBeInTheDocument();
|
|
638
|
+
expect(
|
|
639
|
+
screen.getByText(
|
|
640
|
+
'The /v2/ branch only supports new pages. This page is a legacy page. Please open it from the original entry.',
|
|
641
|
+
),
|
|
642
|
+
).toBeInTheDocument();
|
|
643
|
+
expect(screen.queryByText('404')).not.toBeInTheDocument();
|
|
644
|
+
expect(screen.getByRole('link', { name: 'Open from the original entry' })).toHaveAttribute(
|
|
645
|
+
'href',
|
|
646
|
+
'/nocobase/apps/jhb20/admin/test-page/tab/tab-1?from=direct#dialog',
|
|
647
|
+
);
|
|
637
648
|
expect(replace).not.toHaveBeenCalled();
|
|
638
649
|
expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalled();
|
|
639
650
|
expect(adminLayoutModel.updateRoutePage).not.toHaveBeenCalled();
|
|
@@ -706,6 +717,113 @@ describe('FlowRoute', () => {
|
|
|
706
717
|
}
|
|
707
718
|
});
|
|
708
719
|
|
|
720
|
+
it('should render 404 for missing FlowModel in notFound mode without routeRepository', async () => {
|
|
721
|
+
const engine = new FlowEngine();
|
|
722
|
+
engine.context.defineProperty('app', {
|
|
723
|
+
value: {
|
|
724
|
+
getPublicPath: () => '/v2/',
|
|
725
|
+
router: {
|
|
726
|
+
getBasename: () => '/v2',
|
|
727
|
+
},
|
|
728
|
+
},
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
const adminLayoutModel: MockAdminLayoutModel = Object.assign(
|
|
732
|
+
engine.createModel({ uid: 'admin-layout-model', use: 'FlowModel' }),
|
|
733
|
+
{
|
|
734
|
+
registerRoutePage: vi.fn(),
|
|
735
|
+
updateRoutePage: vi.fn(),
|
|
736
|
+
unregisterRoutePage: vi.fn(),
|
|
737
|
+
},
|
|
738
|
+
);
|
|
739
|
+
|
|
740
|
+
render(
|
|
741
|
+
<FlowEngineProvider engine={engine}>
|
|
742
|
+
<MemoryRouter initialEntries={['/embed/missing-page']}>
|
|
743
|
+
<Routes>
|
|
744
|
+
<Route path="/embed/:name" element={<FlowRoute legacyPageBehavior="notFound" />} />
|
|
745
|
+
</Routes>
|
|
746
|
+
</MemoryRouter>
|
|
747
|
+
</FlowEngineProvider>,
|
|
748
|
+
);
|
|
749
|
+
|
|
750
|
+
expect(await screen.findByText('404')).toBeInTheDocument();
|
|
751
|
+
expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalled();
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
it('should bridge by default when routeRepository does not exist', async () => {
|
|
755
|
+
const engine = new FlowEngine();
|
|
756
|
+
engine.context.defineProperty('app', {
|
|
757
|
+
value: {
|
|
758
|
+
getPublicPath: () => '/v2/',
|
|
759
|
+
router: {
|
|
760
|
+
getBasename: () => '/v2',
|
|
761
|
+
},
|
|
762
|
+
},
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
const adminLayoutModel: MockAdminLayoutModel = Object.assign(
|
|
766
|
+
engine.createModel({ uid: 'admin-layout-model', use: 'FlowModel' }),
|
|
767
|
+
{
|
|
768
|
+
registerRoutePage: vi.fn(),
|
|
769
|
+
updateRoutePage: vi.fn(),
|
|
770
|
+
unregisterRoutePage: vi.fn(),
|
|
771
|
+
},
|
|
772
|
+
);
|
|
773
|
+
|
|
774
|
+
render(
|
|
775
|
+
<FlowEngineProvider engine={engine}>
|
|
776
|
+
<MemoryRouter initialEntries={['/flow/missing-page']}>
|
|
777
|
+
<Routes>
|
|
778
|
+
<Route path="/flow/:name" element={<FlowRoute />} />
|
|
779
|
+
</Routes>
|
|
780
|
+
</MemoryRouter>
|
|
781
|
+
</FlowEngineProvider>,
|
|
782
|
+
);
|
|
783
|
+
|
|
784
|
+
await waitFor(() => {
|
|
785
|
+
expect(adminLayoutModel.registerRoutePage).toHaveBeenCalledWith('missing-page', expect.any(Object));
|
|
786
|
+
});
|
|
787
|
+
expect(screen.queryByText('404')).not.toBeInTheDocument();
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
it('should bridge existing FlowModel in notFound mode without routeRepository', async () => {
|
|
791
|
+
const engine = new FlowEngine();
|
|
792
|
+
engine.createModel({ uid: 'test-page', use: 'FlowModel' });
|
|
793
|
+
engine.context.defineProperty('app', {
|
|
794
|
+
value: {
|
|
795
|
+
getPublicPath: () => '/v2/',
|
|
796
|
+
router: {
|
|
797
|
+
getBasename: () => '/v2',
|
|
798
|
+
},
|
|
799
|
+
},
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
const adminLayoutModel: MockAdminLayoutModel = Object.assign(
|
|
803
|
+
engine.createModel({ uid: 'admin-layout-model', use: 'FlowModel' }),
|
|
804
|
+
{
|
|
805
|
+
registerRoutePage: vi.fn(),
|
|
806
|
+
updateRoutePage: vi.fn(),
|
|
807
|
+
unregisterRoutePage: vi.fn(),
|
|
808
|
+
},
|
|
809
|
+
);
|
|
810
|
+
|
|
811
|
+
render(
|
|
812
|
+
<FlowEngineProvider engine={engine}>
|
|
813
|
+
<MemoryRouter initialEntries={['/embed/test-page']}>
|
|
814
|
+
<Routes>
|
|
815
|
+
<Route path="/embed/:name" element={<FlowRoute legacyPageBehavior="notFound" />} />
|
|
816
|
+
</Routes>
|
|
817
|
+
</MemoryRouter>
|
|
818
|
+
</FlowEngineProvider>,
|
|
819
|
+
);
|
|
820
|
+
|
|
821
|
+
await waitFor(() => {
|
|
822
|
+
expect(adminLayoutModel.registerRoutePage).toHaveBeenCalledWith('test-page', expect.any(Object));
|
|
823
|
+
});
|
|
824
|
+
expect(screen.queryByText('404')).not.toBeInTheDocument();
|
|
825
|
+
});
|
|
826
|
+
|
|
709
827
|
it('should bridge existing FlowModel when behavior is notFound and routeRepository has no route', async () => {
|
|
710
828
|
const engine = new FlowEngine();
|
|
711
829
|
engine.setModelRepository({
|
|
@@ -1801,7 +1919,7 @@ describe('FlowRoute', () => {
|
|
|
1801
1919
|
}
|
|
1802
1920
|
});
|
|
1803
1921
|
|
|
1804
|
-
it('should render
|
|
1922
|
+
it('should render 404 when route and FlowModel do not exist', async () => {
|
|
1805
1923
|
const originalLocation = window.location;
|
|
1806
1924
|
const replace = vi.fn();
|
|
1807
1925
|
Object.defineProperty(window, 'location', {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createMockClient, PluginFlowEngine } from '@nocobase/client-v2';
|
|
11
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
12
|
+
|
|
13
|
+
const { detectedDeviceType } = vi.hoisted(() => ({
|
|
14
|
+
detectedDeviceType: { value: 'mobile' },
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock('react-device-detect', () => ({
|
|
18
|
+
get deviceType() {
|
|
19
|
+
return detectedDeviceType.value;
|
|
20
|
+
},
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
describe('PluginFlowEngine', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
detectedDeviceType.value = 'mobile';
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should register the current device type before shared flow components', async () => {
|
|
29
|
+
const app = createMockClient({
|
|
30
|
+
router: {
|
|
31
|
+
type: 'memory',
|
|
32
|
+
initialEntries: ['/'],
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
const plugin = new PluginFlowEngine({}, app);
|
|
36
|
+
const addComponents = app.addComponents.bind(app);
|
|
37
|
+
const deviceTypesBeforeComponentRegistration: string[] = [];
|
|
38
|
+
vi.spyOn(app, 'addComponents').mockImplementation((components) => {
|
|
39
|
+
deviceTypesBeforeComponentRegistration.push(app.flowEngine.context.deviceType);
|
|
40
|
+
return addComponents(components);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
await plugin.load();
|
|
44
|
+
|
|
45
|
+
expect(app.flowEngine.context.deviceType).toBe('mobile');
|
|
46
|
+
expect(deviceTypesBeforeComponentRegistration).toEqual(['mobile']);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should normalize the browser device type to computer', async () => {
|
|
50
|
+
detectedDeviceType.value = 'browser';
|
|
51
|
+
const app = createMockClient();
|
|
52
|
+
const plugin = new PluginFlowEngine({}, app);
|
|
53
|
+
|
|
54
|
+
await plugin.load();
|
|
55
|
+
|
|
56
|
+
expect(app.flowEngine.context.deviceType).toBe('computer');
|
|
57
|
+
});
|
|
58
|
+
});
|