@asteby/metacore-runtime-react 47.0.0 → 48.0.0
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/CHANGELOG.md +42 -0
- package/dist/action-modal-dispatcher.d.ts.map +1 -1
- package/dist/action-modal-dispatcher.js +24 -4
- package/dist/audit-info.d.ts +54 -0
- package/dist/audit-info.d.ts.map +1 -0
- package/dist/audit-info.js +142 -0
- package/dist/business/catalog-product-search.d.ts +25 -0
- package/dist/business/catalog-product-search.d.ts.map +1 -0
- package/dist/business/catalog-product-search.js +57 -0
- package/dist/business/catalog.d.ts.map +1 -1
- package/dist/business/catalog.js +2 -0
- package/dist/business/document-editor-model.d.ts +107 -0
- package/dist/business/document-editor-model.d.ts.map +1 -0
- package/dist/business/document-editor-model.js +279 -0
- package/dist/business/document-editor-panels.d.ts +64 -0
- package/dist/business/document-editor-panels.d.ts.map +1 -0
- package/dist/business/document-editor-panels.js +58 -0
- package/dist/business/document-editor.d.ts +27 -0
- package/dist/business/document-editor.d.ts.map +1 -0
- package/dist/business/document-editor.js +445 -0
- package/dist/business/document-lines.d.ts.map +1 -1
- package/dist/business/document-lines.js +5 -1
- package/dist/business/index.d.ts +5 -0
- package/dist/business/index.d.ts.map +1 -1
- package/dist/business/index.js +5 -0
- package/dist/business/line-items.d.ts +7 -0
- package/dist/business/line-items.d.ts.map +1 -1
- package/dist/business/line-items.js +6 -1
- package/dist/business/payment-allocator.d.ts +19 -0
- package/dist/business/payment-allocator.d.ts.map +1 -0
- package/dist/business/payment-allocator.js +31 -0
- package/dist/business/product-search.d.ts +4 -0
- package/dist/business/product-search.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +19 -8
- package/dist/document-form-dialog.d.ts +31 -2
- package/dist/document-form-dialog.d.ts.map +1 -1
- package/dist/document-form-dialog.js +43 -11
- package/dist/dynamic-columns-helpers.d.ts +9 -0
- package/dist/dynamic-columns-helpers.d.ts.map +1 -1
- package/dist/dynamic-columns-helpers.js +19 -0
- package/dist/dynamic-columns.d.ts +8 -2
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +21 -5
- package/dist/dynamic-crud-page.d.ts +6 -0
- package/dist/dynamic-crud-page.d.ts.map +1 -1
- package/dist/dynamic-crud-page.js +14 -5
- package/dist/dynamic-form-schema.d.ts.map +1 -1
- package/dist/dynamic-form-schema.js +15 -3
- package/dist/dynamic-form.d.ts +16 -1
- package/dist/dynamic-form.d.ts.map +1 -1
- package/dist/dynamic-form.js +81 -20
- package/dist/dynamic-kanban.d.ts +9 -3
- package/dist/dynamic-kanban.d.ts.map +1 -1
- package/dist/dynamic-kanban.js +39 -13
- package/dist/dynamic-line-items.d.ts.map +1 -1
- package/dist/dynamic-line-items.js +3 -0
- package/dist/dynamic-multi-select-field.d.ts.map +1 -1
- package/dist/dynamic-multi-select-field.js +2 -0
- package/dist/dynamic-relation-helpers.d.ts.map +1 -1
- package/dist/dynamic-relation-helpers.js +8 -0
- package/dist/dynamic-relation.js +1 -1
- package/dist/dynamic-select-field.d.ts.map +1 -1
- package/dist/dynamic-select-field.js +4 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -2
- package/dist/installed-addons-context.d.ts +15 -0
- package/dist/installed-addons-context.d.ts.map +1 -0
- package/dist/installed-addons-context.js +39 -0
- package/dist/option-filter.d.ts +31 -0
- package/dist/option-filter.d.ts.map +1 -0
- package/dist/option-filter.js +49 -0
- package/dist/org-runtime-context.d.ts +8 -0
- package/dist/org-runtime-context.d.ts.map +1 -1
- package/dist/org-runtime-context.js +8 -0
- package/dist/org-runtime-provider.d.ts +3 -1
- package/dist/org-runtime-provider.d.ts.map +1 -1
- package/dist/org-runtime-provider.js +3 -3
- package/dist/primitives/aging.d.ts +34 -0
- package/dist/primitives/aging.d.ts.map +1 -0
- package/dist/primitives/aging.js +48 -0
- package/dist/primitives/allocation.d.ts +42 -0
- package/dist/primitives/allocation.d.ts.map +1 -0
- package/dist/primitives/allocation.js +59 -0
- package/dist/primitives/approvals.d.ts +71 -0
- package/dist/primitives/approvals.d.ts.map +1 -0
- package/dist/primitives/approvals.js +48 -0
- package/dist/primitives/contributions.d.ts +82 -0
- package/dist/primitives/contributions.d.ts.map +1 -0
- package/dist/primitives/contributions.js +126 -0
- package/dist/primitives/document-kinds.d.ts +25 -0
- package/dist/primitives/document-kinds.d.ts.map +1 -0
- package/dist/primitives/document-kinds.js +26 -0
- package/dist/primitives/index.d.ts +8 -0
- package/dist/primitives/index.d.ts.map +1 -0
- package/dist/primitives/index.js +23 -0
- package/dist/primitives/panels.d.ts +68 -0
- package/dist/primitives/panels.d.ts.map +1 -0
- package/dist/primitives/panels.js +3 -0
- package/dist/primitives/use-contributions.d.ts +15 -0
- package/dist/primitives/use-contributions.d.ts.map +1 -0
- package/dist/primitives/use-contributions.js +30 -0
- package/dist/record-prefill-bar.d.ts +11 -0
- package/dist/record-prefill-bar.d.ts.map +1 -0
- package/dist/record-prefill-bar.js +33 -0
- package/dist/types.d.ts +142 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/use-options-resolver.d.ts +8 -0
- package/dist/use-options-resolver.d.ts.map +1 -1
- package/dist/use-options-resolver.js +8 -2
- package/package.json +5 -5
- package/src/__tests__/action-modal-grid-layout.test.tsx +1 -1
- package/src/__tests__/apply-sidebar-layout.test.ts +1 -1
- package/src/__tests__/audit-info.test.tsx +103 -0
- package/src/__tests__/catalog-product-search.test.ts +61 -0
- package/src/__tests__/dependent-options.test.tsx +12 -5
- package/src/__tests__/document-editor-model.test.ts +167 -0
- package/src/__tests__/document-editor.test.tsx +261 -0
- package/src/__tests__/document-form-dialog.test.tsx +118 -3
- package/src/__tests__/dynamic-form-select-edit.test.tsx +155 -0
- package/src/__tests__/dynamic-kanban.test.tsx +24 -3
- package/src/__tests__/dynamic-record-extension-label.test.tsx +88 -0
- package/src/__tests__/dynamic-table-infinite-scroll.test.tsx +4 -0
- package/src/__tests__/installed-addons-context.test.tsx +50 -0
- package/src/__tests__/line-item-totals.test.ts +28 -0
- package/src/__tests__/line-items-lock-rows.test.tsx +2 -1
- package/src/__tests__/option-filter.test.tsx +115 -0
- package/src/__tests__/primitives-federated.test.tsx +156 -0
- package/src/__tests__/primitives.test.ts +224 -0
- package/src/__tests__/record-prefill-bar.test.tsx +50 -0
- package/src/__tests__/structured-view-value.test.tsx +5 -1
- package/src/__tests__/wizard-action-modal.test.tsx +1 -1
- package/src/action-modal-dispatcher.tsx +47 -4
- package/src/audit-info.tsx +219 -0
- package/src/business/catalog-product-search.ts +87 -0
- package/src/business/catalog.ts +2 -0
- package/src/business/document-editor-model.ts +325 -0
- package/src/business/document-editor-panels.tsx +164 -0
- package/src/business/document-editor.tsx +727 -0
- package/src/business/document-lines.ts +5 -1
- package/src/business/index.ts +5 -0
- package/src/business/line-items.ts +15 -1
- package/src/business/payment-allocator.tsx +110 -0
- package/src/business/product-search.ts +4 -0
- package/src/dialogs/dynamic-record.tsx +42 -6
- package/src/document-form-dialog.tsx +92 -12
- package/src/dropdown-menu-entry.test.ts +23 -0
- package/src/dynamic-columns-helpers.ts +22 -0
- package/src/dynamic-columns.tsx +23 -6
- package/src/dynamic-crud-page.tsx +22 -3
- package/src/dynamic-form-schema.ts +15 -2
- package/src/dynamic-form.tsx +120 -39
- package/src/dynamic-kanban.tsx +41 -10
- package/src/dynamic-line-items.tsx +3 -0
- package/src/dynamic-multi-select-field.tsx +2 -0
- package/src/dynamic-relation-helpers.ts +9 -0
- package/src/dynamic-relation.tsx +7 -2
- package/src/dynamic-select-field.tsx +4 -0
- package/src/index.ts +34 -1
- package/src/installed-addons-context.tsx +58 -0
- package/src/option-filter.ts +88 -0
- package/src/org-runtime-context.ts +10 -0
- package/src/org-runtime-provider.tsx +7 -2
- package/src/primitives/aging.ts +76 -0
- package/src/primitives/allocation.ts +104 -0
- package/src/primitives/approvals.ts +103 -0
- package/src/primitives/contributions.ts +208 -0
- package/src/primitives/document-kinds.ts +62 -0
- package/src/primitives/index.ts +23 -0
- package/src/primitives/panels.ts +59 -0
- package/src/primitives/use-contributions.ts +42 -0
- package/src/record-prefill-bar.tsx +62 -0
- package/src/row-action-stage-gate.test.ts +33 -0
- package/src/types.ts +142 -0
- package/src/use-options-resolver.ts +19 -1
- package/vitest.config.ts +1 -0
- package/vitest.setup.ts +7 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
//
|
|
3
|
+
// Editar un registro con un select que YA tiene valor y guardar sin tocarlo debe
|
|
4
|
+
// conservar ese valor en el payload. Antes los `values` del DynamicForm se
|
|
5
|
+
// sembraban en un efecto (primer render con '') y el Select de Radix emitía
|
|
6
|
+
// `onValueChange('')`, que pisaba el valor real y viajaba en el PATCH (p.ej. un
|
|
7
|
+
// renglón «Refacción» quedaba en «-»). Cubre también: `readonly` de columna en
|
|
8
|
+
// `deriveRelationFormFields`, selects vacíos no tocados fuera del PATCH, y el
|
|
9
|
+
// modal de renglón con cuerpo con scroll y footer fijo.
|
|
10
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
11
|
+
import { act, cleanup, render, screen, waitFor } from '@testing-library/react'
|
|
12
|
+
|
|
13
|
+
vi.mock('react-i18next', () => ({
|
|
14
|
+
useTranslation: () => ({ t: (k: string, o?: any) => o?.defaultValue ?? k, i18n: { language: 'es' } }),
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
import { DynamicForm, guardEmptySelect } from '../dynamic-form'
|
|
18
|
+
import { DynamicRelation } from '../dynamic-relation'
|
|
19
|
+
import { deriveRelationFormFields } from '../dynamic-relation-helpers'
|
|
20
|
+
import { ApiProvider, type ApiClient } from '../api-context'
|
|
21
|
+
import type { ActionFieldDef } from '../types'
|
|
22
|
+
|
|
23
|
+
afterEach(cleanup)
|
|
24
|
+
|
|
25
|
+
const kindField: ActionFieldDef = {
|
|
26
|
+
key: 'kind',
|
|
27
|
+
label: 'Tipo',
|
|
28
|
+
type: 'select',
|
|
29
|
+
options: [
|
|
30
|
+
{ value: 'service', label: 'Servicio' },
|
|
31
|
+
{ value: 'part', label: 'Refacción' },
|
|
32
|
+
],
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('DynamicForm — edición con select', () => {
|
|
36
|
+
it('guardar sin tocar el select conserva su valor en el payload', async () => {
|
|
37
|
+
const onSubmit = vi.fn()
|
|
38
|
+
render(
|
|
39
|
+
<DynamicForm
|
|
40
|
+
fields={[{ key: 'description', label: 'Descripción', type: 'text' }, kindField]}
|
|
41
|
+
initialValues={{ id: 'r1', description: 'Balata', kind: 'part' }}
|
|
42
|
+
onSubmit={onSubmit}
|
|
43
|
+
/>,
|
|
44
|
+
)
|
|
45
|
+
await act(async () => {
|
|
46
|
+
screen.getByText('Guardar').click()
|
|
47
|
+
})
|
|
48
|
+
expect(onSubmit).toHaveBeenCalledTimes(1)
|
|
49
|
+
expect(onSubmit.mock.calls[0][0]).toMatchObject({ description: 'Balata', kind: 'part' })
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('en edición no manda un select vacío que el usuario no tocó', async () => {
|
|
53
|
+
const onSubmit = vi.fn()
|
|
54
|
+
render(
|
|
55
|
+
<DynamicForm
|
|
56
|
+
fields={[{ key: 'description', label: 'Descripción', type: 'text' }, kindField]}
|
|
57
|
+
initialValues={{ id: 'r1', description: 'Balata' }}
|
|
58
|
+
onSubmit={onSubmit}
|
|
59
|
+
/>,
|
|
60
|
+
)
|
|
61
|
+
await act(async () => {
|
|
62
|
+
screen.getByText('Guardar').click()
|
|
63
|
+
})
|
|
64
|
+
expect(onSubmit).toHaveBeenCalledTimes(1)
|
|
65
|
+
expect(onSubmit.mock.calls[0][0]).not.toHaveProperty('kind')
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe('guardEmptySelect', () => {
|
|
70
|
+
it("ignora '' cuando ya hay valor y '' no es opción", () => {
|
|
71
|
+
const onChange = vi.fn()
|
|
72
|
+
guardEmptySelect('part', onChange, ['service', 'part'])('')
|
|
73
|
+
expect(onChange).not.toHaveBeenCalled()
|
|
74
|
+
})
|
|
75
|
+
it("deja pasar '' si no había valor o si '' es opción válida, y cualquier otro valor", () => {
|
|
76
|
+
const onChange = vi.fn()
|
|
77
|
+
guardEmptySelect('', onChange, ['service', 'part'])('')
|
|
78
|
+
guardEmptySelect('part', onChange, ['', 'part'])('')
|
|
79
|
+
guardEmptySelect('part', onChange, ['service', 'part'])('service')
|
|
80
|
+
expect(onChange.mock.calls).toEqual([[''], [''], ['service']])
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const META = {
|
|
85
|
+
name: 'work_order_item',
|
|
86
|
+
columns: [
|
|
87
|
+
{ key: 'id', label: 'ID', type: 'text', sortable: true, filterable: false, hidden: true },
|
|
88
|
+
{ key: 'work_order_id', label: 'OT', type: 'text', sortable: false, filterable: false },
|
|
89
|
+
{ key: 'description', label: 'Descripción', type: 'text', sortable: true, filterable: true },
|
|
90
|
+
{
|
|
91
|
+
key: 'kind',
|
|
92
|
+
label: 'Tipo',
|
|
93
|
+
type: 'select',
|
|
94
|
+
sortable: true,
|
|
95
|
+
filterable: true,
|
|
96
|
+
options: [
|
|
97
|
+
{ value: 'service', label: 'Servicio' },
|
|
98
|
+
{ value: 'part', label: 'Refacción' },
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
{ key: 'line_total', label: 'Importe', type: 'number', sortable: false, filterable: false, readonly: true },
|
|
102
|
+
],
|
|
103
|
+
actions: [],
|
|
104
|
+
hasActions: false,
|
|
105
|
+
enableCRUDActions: false,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const DATA = [{ id: 'woi_1', work_order_id: 'wo_1', description: 'Balata', kind: 'part', line_total: 450 }]
|
|
109
|
+
|
|
110
|
+
function mockApi(): ApiClient {
|
|
111
|
+
return {
|
|
112
|
+
get: vi.fn((url: string) => {
|
|
113
|
+
if (url.startsWith('/metadata/table/')) {
|
|
114
|
+
return Promise.resolve({ data: { success: true, data: META } })
|
|
115
|
+
}
|
|
116
|
+
return Promise.resolve({ data: { success: true, data: DATA } })
|
|
117
|
+
}),
|
|
118
|
+
post: vi.fn(() => Promise.resolve({ data: { success: true, data: {} } })),
|
|
119
|
+
put: vi.fn(() => Promise.resolve({ data: { success: true, data: {} } })),
|
|
120
|
+
delete: vi.fn(() => Promise.resolve({ data: { success: true, data: {} } })),
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
describe('DynamicRelation — editar renglón con select', () => {
|
|
125
|
+
it('deriveRelationFormFields copia readonly de la columna', () => {
|
|
126
|
+
const fields = deriveRelationFormFields(META as any, 'work_order_id')
|
|
127
|
+
expect(fields.find((f) => f.key === 'line_total')?.readonly).toBe(true)
|
|
128
|
+
expect(fields.find((f) => f.key === 'kind')?.readonly).toBeUndefined()
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('el PATCH conserva el tipo y no manda columnas readonly; el modal hace scroll con footer fijo', async () => {
|
|
132
|
+
const api = mockApi()
|
|
133
|
+
render(
|
|
134
|
+
<ApiProvider client={api}>
|
|
135
|
+
<DynamicRelation kind="one_to_many" model="work_order_item" foreignKey="work_order_id" parentId="wo_1" />
|
|
136
|
+
</ApiProvider>,
|
|
137
|
+
)
|
|
138
|
+
await waitFor(() => expect(screen.getByText('Balata')).toBeTruthy())
|
|
139
|
+
await act(async () => {
|
|
140
|
+
screen.getByLabelText('Editar').click()
|
|
141
|
+
})
|
|
142
|
+
const dialog = await screen.findByRole('dialog')
|
|
143
|
+
expect(dialog.className).toContain('max-h-[90dvh]')
|
|
144
|
+
expect(dialog.querySelector('.overflow-y-auto')).toBeTruthy()
|
|
145
|
+
|
|
146
|
+
await act(async () => {
|
|
147
|
+
screen.getByText('Guardar').click()
|
|
148
|
+
})
|
|
149
|
+
await waitFor(() => expect(api.put).toHaveBeenCalledTimes(1))
|
|
150
|
+
const [url, payload] = (api.put as any).mock.calls[0]
|
|
151
|
+
expect(url).toContain('woi_1')
|
|
152
|
+
expect(payload).toMatchObject({ description: 'Balata', kind: 'part' })
|
|
153
|
+
expect(payload).not.toHaveProperty('line_total')
|
|
154
|
+
})
|
|
155
|
+
})
|
|
@@ -293,11 +293,32 @@ describe('withGroupBy (one model, several boards)', () => {
|
|
|
293
293
|
...over,
|
|
294
294
|
}) as any
|
|
295
295
|
|
|
296
|
-
it('is the same object when
|
|
297
|
-
const m = boardMeta()
|
|
296
|
+
it('is the same object when the served group_by is already the stage column', () => {
|
|
297
|
+
const m = boardMeta({ group_by: 'stage' })
|
|
298
298
|
expect(withGroupBy(m, undefined)).toBe(m)
|
|
299
|
-
expect(withGroupBy(m, '
|
|
299
|
+
expect(withGroupBy(m, 'stage')).toBe(m)
|
|
300
300
|
expect(withGroupBy(null, 'stage')).toBeNull()
|
|
301
|
+
const flat = boardMeta({ stages: undefined, transitions: undefined, smart_lanes: undefined })
|
|
302
|
+
expect(withGroupBy(flat, 'assigned_mechanic_id')).toBe(flat)
|
|
303
|
+
expect(withGroupBy(flat, undefined)).toBe(flat)
|
|
304
|
+
})
|
|
305
|
+
// Pitsline r3 (b): WorkOrder serves group_by=service_bay_id (the last kanban
|
|
306
|
+
// nav entry) with the stage lanes. Without ?group_by= the board grouped the
|
|
307
|
+
// stage lanes by the bay: every card in «Sin etapa», each lane count sent
|
|
308
|
+
// f_service_bay_id=diagnosis and a drop PUT service_bay_id='diagnosis' (422).
|
|
309
|
+
it('groups a board without ?group_by= by the stage column, not the served one', () => {
|
|
310
|
+
const out = withGroupBy(boardMeta(), undefined)!
|
|
311
|
+
expect(out.group_by).toBe('stage')
|
|
312
|
+
expect(out.stages).toHaveLength(1)
|
|
313
|
+
expect(out.transitions).toHaveLength(1)
|
|
314
|
+
})
|
|
315
|
+
it('drops the stage machine on the board of the served (non-stage) column', () => {
|
|
316
|
+
const out = withGroupBy(boardMeta(), 'assigned_mechanic_id')!
|
|
317
|
+
expect(out.group_by).toBe('assigned_mechanic_id')
|
|
318
|
+
expect(out.stages).toBeUndefined()
|
|
319
|
+
expect(out.transitions).toBeUndefined()
|
|
320
|
+
expect(out.smart_lanes).toBeUndefined()
|
|
321
|
+
expect(refLaneSource(out)).toEqual({ ref: 'users' })
|
|
301
322
|
})
|
|
302
323
|
it('ignores a column the model does not have', () => {
|
|
303
324
|
const m = boardMeta()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
//
|
|
3
|
+
// Retest Pitsline 05/10: the customer and product forms printed the fields that
|
|
4
|
+
// fiscal_mexico adds by extension as `FISCAL_MEXICO.EXT.CUSTOMER.RFC_RECEPTOR`.
|
|
5
|
+
// The server served the label as the raw catalog key and the dialog rendered it
|
|
6
|
+
// verbatim (uppercased by CSS). The dialog now resolves such a key with the
|
|
7
|
+
// client catalog, and humanizes it when the catalog does not have it either.
|
|
8
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
9
|
+
import { cleanup, render, waitFor } from '@testing-library/react'
|
|
10
|
+
import { I18nextProvider } from 'react-i18next'
|
|
11
|
+
import i18next from 'i18next'
|
|
12
|
+
|
|
13
|
+
import { DynamicRecordDialog } from '../dialogs/dynamic-record'
|
|
14
|
+
import { ApiProvider } from '../api-context'
|
|
15
|
+
import { localizeFieldLabel } from '../dynamic-columns-helpers'
|
|
16
|
+
|
|
17
|
+
afterEach(cleanup)
|
|
18
|
+
|
|
19
|
+
function makeI18n() {
|
|
20
|
+
const inst = i18next.createInstance()
|
|
21
|
+
void inst.init({
|
|
22
|
+
lng: 'es',
|
|
23
|
+
fallbackLng: 'es',
|
|
24
|
+
react: { useSuspense: false },
|
|
25
|
+
resources: {
|
|
26
|
+
es: {
|
|
27
|
+
translation: {
|
|
28
|
+
fiscal_mexico: { ext: { customer: { rfc_receptor: 'RFC' } } },
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
return inst
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const api = {
|
|
37
|
+
get: vi.fn(async () => ({ data: { data: {} } })),
|
|
38
|
+
post: async () => ({ data: { success: true } }),
|
|
39
|
+
put: async () => ({ data: { success: true } }),
|
|
40
|
+
delete: async () => ({ data: { success: true } }),
|
|
41
|
+
} as never
|
|
42
|
+
|
|
43
|
+
describe('DynamicRecordDialog — extension field labels', () => {
|
|
44
|
+
it('never prints a raw catalog key as the field label', async () => {
|
|
45
|
+
render(
|
|
46
|
+
<I18nextProvider i18n={makeI18n()}>
|
|
47
|
+
<ApiProvider client={api}>
|
|
48
|
+
<DynamicRecordDialog
|
|
49
|
+
open
|
|
50
|
+
onOpenChange={() => {}}
|
|
51
|
+
mode="create"
|
|
52
|
+
model="customers"
|
|
53
|
+
schema={{
|
|
54
|
+
title: 'Nuevo cliente',
|
|
55
|
+
fields: [
|
|
56
|
+
{ key: 'name', label: 'Nombre', type: 'text' },
|
|
57
|
+
{ key: 'fiscal_data.rfc_receptor', label: 'fiscal_mexico.ext.customer.rfc_receptor', type: 'text' },
|
|
58
|
+
{ key: 'fiscal_data.codigo_postal', label: 'fiscal_mexico.ext.customer.codigo_postal', type: 'text' },
|
|
59
|
+
],
|
|
60
|
+
} as never}
|
|
61
|
+
/>
|
|
62
|
+
</ApiProvider>
|
|
63
|
+
</I18nextProvider>,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
const label = (key: string) =>
|
|
67
|
+
document.querySelector(`[data-aby-field="${key}"] label`)?.textContent ?? ''
|
|
68
|
+
await waitFor(() => expect(label('fiscal_data.rfc_receptor')).toBe('RFC'))
|
|
69
|
+
expect(label('fiscal_data.codigo_postal')).toBe('Codigo Postal')
|
|
70
|
+
expect(label('name')).toBe('Nombre')
|
|
71
|
+
expect(document.body.textContent).not.toContain('fiscal_mexico.ext')
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe('localizeFieldLabel', () => {
|
|
76
|
+
it('leaves human labels alone, even single words or acronyms', () => {
|
|
77
|
+
const t = vi.fn((k: string) => `T:${k}`)
|
|
78
|
+
expect(localizeFieldLabel('Nombre', t)).toBe('Nombre')
|
|
79
|
+
expect(localizeFieldLabel('RFC', t)).toBe('RFC')
|
|
80
|
+
expect(localizeFieldLabel('Código postal', t)).toBe('Código postal')
|
|
81
|
+
expect(t).not.toHaveBeenCalled()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('resolves catalog keys and humanizes the ones the catalog lacks', () => {
|
|
85
|
+
expect(localizeFieldLabel('a.b', (k: string, o?: { defaultValue?: string }) => (k === 'a.b' ? 'Hola' : o?.defaultValue ?? k))).toBe('Hola')
|
|
86
|
+
expect(localizeFieldLabel('fiscal_mexico.ext.product.mx_clave_unidad', (k: string) => k)).toBe('Mx Clave Unidad')
|
|
87
|
+
})
|
|
88
|
+
})
|
|
@@ -149,6 +149,10 @@ describe('DynamicTable infinite scroll', () => {
|
|
|
149
149
|
await waitFor(() => expect(screen.getByText('30 de 45')).toBeTruthy())
|
|
150
150
|
observers.forEach((o) => o.fire(true))
|
|
151
151
|
await waitFor(() => expect(screen.getByText('45 de 45')).toBeTruthy())
|
|
152
|
+
// The sentinel leaves the viewport once everything is loaded. While it
|
|
153
|
+
// stays intersecting the hook deliberately chains the next page after a
|
|
154
|
+
// reset (viewport-fill), which would mask the collapse asserted below.
|
|
155
|
+
observers.forEach((o) => o.fire(false))
|
|
152
156
|
|
|
153
157
|
// Change the active filters via defaultFilters (part of buildFilterParams).
|
|
154
158
|
rerender(
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
//
|
|
3
|
+
// InstalledAddonsProvider: sin provider = desconocido (undefined), con provider
|
|
4
|
+
// responde por clave de addon y por capacidad.
|
|
5
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
6
|
+
import { cleanup, render, screen } from '@testing-library/react'
|
|
7
|
+
import {
|
|
8
|
+
InstalledAddonsProvider,
|
|
9
|
+
useAddonInstalled,
|
|
10
|
+
useCapabilityProvided,
|
|
11
|
+
useInstalledAddons,
|
|
12
|
+
} from '../installed-addons-context'
|
|
13
|
+
|
|
14
|
+
afterEach(() => cleanup())
|
|
15
|
+
|
|
16
|
+
function Probe() {
|
|
17
|
+
const fiscal = useAddonInstalled('fiscal_mexico')
|
|
18
|
+
const issuer = useCapabilityProvided('fiscal.invoice_issuer')
|
|
19
|
+
const all = useInstalledAddons()
|
|
20
|
+
return (
|
|
21
|
+
<span data-testid="probe">
|
|
22
|
+
{String(fiscal)}|{String(issuer)}|{all ? [...all.addons].join(',') : 'null'}
|
|
23
|
+
</span>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('InstalledAddonsProvider', () => {
|
|
28
|
+
it('sin provider devuelve undefined/null (nunca asume ausente)', () => {
|
|
29
|
+
render(<Probe />)
|
|
30
|
+
expect(screen.getByTestId('probe').textContent).toBe('undefined|undefined|null')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('responde por addon y por capacidad', () => {
|
|
34
|
+
render(
|
|
35
|
+
<InstalledAddonsProvider addons={['customers', 'fiscal_mexico']} capabilities={['fiscal.invoice_issuer']}>
|
|
36
|
+
<Probe />
|
|
37
|
+
</InstalledAddonsProvider>,
|
|
38
|
+
)
|
|
39
|
+
expect(screen.getByTestId('probe').textContent).toBe('true|true|customers,fiscal_mexico')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('addon no instalado = false', () => {
|
|
43
|
+
render(
|
|
44
|
+
<InstalledAddonsProvider addons={['customers']}>
|
|
45
|
+
<Probe />
|
|
46
|
+
</InstalledAddonsProvider>,
|
|
47
|
+
)
|
|
48
|
+
expect(screen.getByTestId('probe').textContent).toBe('false|false|customers')
|
|
49
|
+
})
|
|
50
|
+
})
|
|
@@ -25,6 +25,34 @@ const journalField = (overrides: Partial<ActionFieldDef> = {}): ActionFieldDef =
|
|
|
25
25
|
})
|
|
26
26
|
|
|
27
27
|
describe('computeLineItemTotals', () => {
|
|
28
|
+
// Pitsline r3 (g): the work order lines carry qty + a `total` price column
|
|
29
|
+
// and no amount column. qty=2 × 750 footed 750; the saved order was 1500.
|
|
30
|
+
it('suma qty × precio cuando la columna total es el precio unitario', () => {
|
|
31
|
+
const field = {
|
|
32
|
+
key: 'lines',
|
|
33
|
+
type: 'array',
|
|
34
|
+
itemFields: [
|
|
35
|
+
{ key: 'description', type: 'string' },
|
|
36
|
+
{ key: 'qty', type: 'number' },
|
|
37
|
+
{ key: 'price', type: 'number', total: true },
|
|
38
|
+
],
|
|
39
|
+
} as any
|
|
40
|
+
const rows = [{ qty: '2', price: '750' }, { qty: 1, price: 99.9 }]
|
|
41
|
+
expect(computeLineItemTotals(field, rows)).toEqual({ price: 1599.9 })
|
|
42
|
+
})
|
|
43
|
+
it('con columna de importe, el precio unitario se suma tal cual', () => {
|
|
44
|
+
const field = {
|
|
45
|
+
key: 'lines',
|
|
46
|
+
type: 'array',
|
|
47
|
+
itemFields: [
|
|
48
|
+
{ key: 'qty', type: 'number' },
|
|
49
|
+
{ key: 'unit_price', type: 'number', total: true },
|
|
50
|
+
{ key: 'subtotal', type: 'number', total: true },
|
|
51
|
+
],
|
|
52
|
+
} as any
|
|
53
|
+
const rows = [{ qty: 2, unit_price: 10, subtotal: 20 }]
|
|
54
|
+
expect(computeLineItemTotals(field, rows)).toEqual({ unit_price: 10, subtotal: 20 })
|
|
55
|
+
})
|
|
28
56
|
it('suma solo las columnas marcadas con total', () => {
|
|
29
57
|
const rows = [
|
|
30
58
|
{ account_id: 'a', debit: '100', credit: '' },
|
|
@@ -65,6 +65,7 @@ describe('DynamicLineItems — lock_rows', () => {
|
|
|
65
65
|
it('control: sin lock_rows muestra "Agregar renglón" y un botón de borrar por fila', () => {
|
|
66
66
|
render(<DynamicLineItems field={lineField()} value={rows} onChange={vi.fn()} />)
|
|
67
67
|
expect(screen.getByText('Agregar renglón')).toBeTruthy()
|
|
68
|
-
|
|
68
|
+
// Mobile cards + desktop table both render a delete control per row.
|
|
69
|
+
expect(screen.getAllByLabelText('Eliminar renglón')).toHaveLength(rows.length * 2)
|
|
69
70
|
})
|
|
70
71
|
})
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
//
|
|
3
|
+
// `option_filter` / `optionFilter` on a relation picker hides options client-side
|
|
4
|
+
// (PIT-059: cancelled invoices must not show up in the payments selector).
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
6
|
+
import { act, cleanup, render, screen, waitFor, within } from '@testing-library/react'
|
|
7
|
+
import { applyOptionFilter, getOptionFilter, optionPassesRule } from '../option-filter'
|
|
8
|
+
import { projectOption } from '../use-options-resolver'
|
|
9
|
+
import { DynamicSelectField } from '../dynamic-select-field'
|
|
10
|
+
import { ApiProvider, type ApiClient } from '../api-context'
|
|
11
|
+
import type { ActionFieldDef } from '../types'
|
|
12
|
+
|
|
13
|
+
vi.mock('react-i18next', () => ({
|
|
14
|
+
useTranslation: () => ({ t: (k: string, o?: any) => o?.defaultValue ?? k, i18n: { language: 'es' } }),
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
afterEach(cleanup)
|
|
18
|
+
|
|
19
|
+
const invoices = [
|
|
20
|
+
{ id: 'i1', label: 'FAC-1', status: 'timbrada', total: 100 },
|
|
21
|
+
{ id: 'i2', label: 'FAC-2', status: 'Cancelada', total: 50 },
|
|
22
|
+
{ id: 'i3', label: 'FAC-3', status: 'pendiente', total: 70 },
|
|
23
|
+
{ id: 'i4', label: 'FAC-4' },
|
|
24
|
+
].map(projectOption)
|
|
25
|
+
|
|
26
|
+
describe('getOptionFilter', () => {
|
|
27
|
+
it('is empty when the field declares none (retrocompat)', () => {
|
|
28
|
+
expect(getOptionFilter({ key: 'x' })).toEqual([])
|
|
29
|
+
expect(getOptionFilter(undefined)).toEqual([])
|
|
30
|
+
})
|
|
31
|
+
it('reads camelCase and snake_case, one rule or a list', () => {
|
|
32
|
+
const rule = { field: 'status', not_in: ['cancelada'] }
|
|
33
|
+
expect(getOptionFilter({ optionFilter: rule })).toEqual([rule])
|
|
34
|
+
expect(getOptionFilter({ option_filter: [rule] })).toEqual([rule])
|
|
35
|
+
})
|
|
36
|
+
it('ignores malformed rules', () => {
|
|
37
|
+
expect(getOptionFilter({ option_filter: [{ nope: 1 }, null, { field: '' }] })).toEqual([])
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('applyOptionFilter', () => {
|
|
42
|
+
it('returns the same array with no rules', () => {
|
|
43
|
+
expect(applyOptionFilter(invoices, [])).toBe(invoices)
|
|
44
|
+
})
|
|
45
|
+
it('not_in hides cancelled invoices (case-insensitive) and keeps options without the column', () => {
|
|
46
|
+
const out = applyOptionFilter(invoices, [{ field: 'status', not_in: ['cancelada'] }])
|
|
47
|
+
expect(out.map((o) => o.id)).toEqual(['i1', 'i3', 'i4'])
|
|
48
|
+
})
|
|
49
|
+
it('notEquals alias and equals / in need the column to exist', () => {
|
|
50
|
+
expect(applyOptionFilter(invoices, [{ field: 'status', notEquals: 'cancelada' }]).map((o) => o.id)).toEqual(['i1', 'i3', 'i4'])
|
|
51
|
+
expect(applyOptionFilter(invoices, [{ field: 'status', equals: 'timbrada' }]).map((o) => o.id)).toEqual(['i1'])
|
|
52
|
+
expect(applyOptionFilter(invoices, [{ field: 'status', in: ['timbrada', 'pendiente'] }]).map((o) => o.id)).toEqual(['i1', 'i3'])
|
|
53
|
+
})
|
|
54
|
+
it('combines rules with AND', () => {
|
|
55
|
+
const out = applyOptionFilter(invoices, [
|
|
56
|
+
{ field: 'status', not_in: ['cancelada'] },
|
|
57
|
+
{ field: 'status', not_equals: 'pendiente' },
|
|
58
|
+
])
|
|
59
|
+
expect(out.map((o) => o.id)).toEqual(['i1', 'i4'])
|
|
60
|
+
})
|
|
61
|
+
it('never hides the current selection', () => {
|
|
62
|
+
const out = applyOptionFilter(invoices, [{ field: 'status', not_in: ['cancelada'] }], 'i2')
|
|
63
|
+
expect(out.map((o) => o.id)).toEqual(['i1', 'i2', 'i3', 'i4'])
|
|
64
|
+
})
|
|
65
|
+
it('tests standard keys too (label)', () => {
|
|
66
|
+
expect(optionPassesRule(invoices[0], { field: 'label', equals: 'fac-1' })).toBe(true)
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe('DynamicSelectField with option_filter', () => {
|
|
71
|
+
const field = (extra: Partial<ActionFieldDef> = {}): ActionFieldDef => ({
|
|
72
|
+
key: 'invoice_id',
|
|
73
|
+
label: 'Factura',
|
|
74
|
+
type: 'dynamic_select',
|
|
75
|
+
ref: 'invoices.Invoice',
|
|
76
|
+
...extra,
|
|
77
|
+
})
|
|
78
|
+
const api = () => {
|
|
79
|
+
const get = vi.fn(async () => ({
|
|
80
|
+
data: {
|
|
81
|
+
success: true,
|
|
82
|
+
data: [
|
|
83
|
+
{ id: 'i1', label: 'FAC-1', status: 'timbrada' },
|
|
84
|
+
{ id: 'i2', label: 'FAC-2', status: 'cancelada' },
|
|
85
|
+
],
|
|
86
|
+
meta: { type: 'dynamic', count: 2 },
|
|
87
|
+
},
|
|
88
|
+
}))
|
|
89
|
+
const post = vi.fn(async () => ({ data: { success: false } }))
|
|
90
|
+
return { get, post } as unknown as ApiClient
|
|
91
|
+
}
|
|
92
|
+
const open = async (f: ActionFieldDef) => {
|
|
93
|
+
render(
|
|
94
|
+
<ApiProvider client={api()}>
|
|
95
|
+
<DynamicSelectField field={f} value="" onChange={() => {}} />
|
|
96
|
+
</ApiProvider>,
|
|
97
|
+
)
|
|
98
|
+
await act(async () => {
|
|
99
|
+
screen.getByRole('combobox').click()
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
it('lists every option without a filter', async () => {
|
|
104
|
+
await open(field())
|
|
105
|
+
await waitFor(() => expect(screen.getByText('FAC-2')).toBeTruthy())
|
|
106
|
+
expect(screen.getByText('FAC-1')).toBeTruthy()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('hides the cancelled invoice when the manifest declares option_filter', async () => {
|
|
110
|
+
await open(field({ option_filter: { field: 'status', not_in: ['cancelada'] } }))
|
|
111
|
+
await waitFor(() => expect(screen.getByText('FAC-1')).toBeTruthy())
|
|
112
|
+
expect(screen.queryByText('FAC-2')).toBeNull()
|
|
113
|
+
expect(within(document.body).queryAllByText('FAC-2')).toHaveLength(0)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
//
|
|
3
|
+
// Primitivos federados en React: el dispatcher pinta un modal de
|
|
4
|
+
// registerFederatedModal sin esperar al host, usa el formulario genérico de
|
|
5
|
+
// inmediato si el addon del slug no está instalado, y useDocumentContributions
|
|
6
|
+
// re-pinta al registrar/desmontar un remote.
|
|
7
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
8
|
+
import { act, cleanup, render, screen } from '@testing-library/react'
|
|
9
|
+
|
|
10
|
+
const USE_TRANSLATION = {
|
|
11
|
+
t: (_k: string, opts?: { defaultValue?: string }) => opts?.defaultValue ?? _k,
|
|
12
|
+
i18n: { language: 'es' },
|
|
13
|
+
}
|
|
14
|
+
vi.mock('react-i18next', () => ({ useTranslation: () => USE_TRANSLATION }))
|
|
15
|
+
vi.mock('@tanstack/react-router', () => ({ useNavigate: () => () => {} }))
|
|
16
|
+
|
|
17
|
+
import { ActionModalDispatcher } from '../action-modal-dispatcher'
|
|
18
|
+
import { setFederatedActionLoader } from '../federated-action-loader'
|
|
19
|
+
import { ApiProvider, type ApiClient } from '../api-context'
|
|
20
|
+
import { InstalledAddonsProvider } from '../installed-addons-context'
|
|
21
|
+
import {
|
|
22
|
+
__resetContributions,
|
|
23
|
+
registerDocumentContribution,
|
|
24
|
+
registerFederatedModal,
|
|
25
|
+
type DocumentEditorContext,
|
|
26
|
+
} from '../primitives/contributions'
|
|
27
|
+
import { useDocumentContributions } from '../primitives/use-contributions'
|
|
28
|
+
import type { ActionMetadata, ActionModalProps } from '@asteby/metacore-sdk'
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
cleanup()
|
|
32
|
+
vi.restoreAllMocks()
|
|
33
|
+
setFederatedActionLoader(null)
|
|
34
|
+
__resetContributions()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const noopApi = {
|
|
38
|
+
get: vi.fn().mockResolvedValue({ data: {} }),
|
|
39
|
+
post: vi.fn().mockResolvedValue({ data: { success: true } }),
|
|
40
|
+
delete: vi.fn().mockResolvedValue({ data: { success: true } }),
|
|
41
|
+
} as unknown as ApiClient
|
|
42
|
+
|
|
43
|
+
const importAction: ActionMetadata = {
|
|
44
|
+
key: 'import_cfdi',
|
|
45
|
+
label: 'Importar CFDI',
|
|
46
|
+
modal: 'fiscal_mexico.import_cfdi',
|
|
47
|
+
confirm: true,
|
|
48
|
+
confirmMessage: '¿Importar?',
|
|
49
|
+
executable: true,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function ImportModal({ action }: ActionModalProps) {
|
|
53
|
+
return <div data-testid="import-modal">{action.modal}</div>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function renderDispatcher(addons?: string[]) {
|
|
57
|
+
const tree = (
|
|
58
|
+
<ActionModalDispatcher
|
|
59
|
+
open
|
|
60
|
+
onOpenChange={() => {}}
|
|
61
|
+
action={importAction}
|
|
62
|
+
model="Invoice"
|
|
63
|
+
record={{ id: '1' } as any}
|
|
64
|
+
endpoint="/data/Invoice"
|
|
65
|
+
onSuccess={() => {}}
|
|
66
|
+
/>
|
|
67
|
+
)
|
|
68
|
+
return render(
|
|
69
|
+
<ApiProvider client={noopApi}>
|
|
70
|
+
{addons ? <InstalledAddonsProvider addons={addons}>{tree}</InstalledAddonsProvider> : tree}
|
|
71
|
+
</ApiProvider>,
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe('ActionModalDispatcher + registerFederatedModal', () => {
|
|
76
|
+
it('pinta el modal registrado (lazy) sin pedir el remote al host', async () => {
|
|
77
|
+
const loader = vi.fn()
|
|
78
|
+
setFederatedActionLoader(loader)
|
|
79
|
+
registerFederatedModal({ key: 'fiscal_mexico.import_cfdi', addon: 'fiscal_mexico', load: async () => ({ default: ImportModal }) })
|
|
80
|
+
renderDispatcher(['fiscal_mexico'])
|
|
81
|
+
expect((await screen.findByTestId('import-modal')).textContent).toBe('fiscal_mexico.import_cfdi')
|
|
82
|
+
expect(loader).not.toHaveBeenCalled()
|
|
83
|
+
expect(screen.queryByText('¿Importar?')).toBeNull()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('addon del slug no instalado → formulario genérico de inmediato, sin esperar ni pedir el remote', () => {
|
|
87
|
+
const loader = vi.fn()
|
|
88
|
+
setFederatedActionLoader(loader)
|
|
89
|
+
registerFederatedModal({ key: 'fiscal_mexico.import_cfdi', addon: 'fiscal_mexico', load: async () => ({ default: ImportModal }) })
|
|
90
|
+
renderDispatcher(['customers'])
|
|
91
|
+
expect(screen.getByText('¿Importar?')).toBeTruthy()
|
|
92
|
+
expect(screen.queryByTestId('federated-action-loading')).toBeNull()
|
|
93
|
+
expect(loader).not.toHaveBeenCalled()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('addon instalado pero modal aún sin registrar → espera al remote (no cae al genérico)', async () => {
|
|
97
|
+
renderDispatcher(['fiscal_mexico'])
|
|
98
|
+
expect(screen.getByTestId('federated-action-loading')).toBeTruthy()
|
|
99
|
+
expect(screen.queryByText('¿Importar?')).toBeNull()
|
|
100
|
+
act(() => {
|
|
101
|
+
registerFederatedModal({ key: 'fiscal_mexico.import_cfdi', addon: 'fiscal_mexico', load: async () => ({ default: ImportModal }) })
|
|
102
|
+
})
|
|
103
|
+
expect(await screen.findByTestId('import-modal')).toBeTruthy()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('sin InstalledAddonsProvider se conserva la espera de siempre', () => {
|
|
107
|
+
renderDispatcher()
|
|
108
|
+
expect(screen.getByTestId('federated-action-loading')).toBeTruthy()
|
|
109
|
+
expect(screen.queryByText('¿Importar?')).toBeNull()
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
describe('useDocumentContributions', () => {
|
|
114
|
+
const ctx: DocumentEditorContext = { kind: 'invoice', model: 'customers.Invoice', values: {}, mode: 'create' }
|
|
115
|
+
|
|
116
|
+
function Region() {
|
|
117
|
+
const items = useDocumentContributions(ctx, 'header.fields')
|
|
118
|
+
return (
|
|
119
|
+
<div data-testid="region">
|
|
120
|
+
{items.map(({ id, component: C }) => (
|
|
121
|
+
<C key={id} ctx={ctx} setValue={() => {}} issues={{ report: () => {} }} />
|
|
122
|
+
))}
|
|
123
|
+
</div>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
it('re-pinta al registrar y al desmontar el remote; filtra por addon instalado', async () => {
|
|
128
|
+
render(
|
|
129
|
+
<InstalledAddonsProvider addons={['fiscal_mexico']}>
|
|
130
|
+
<Region />
|
|
131
|
+
</InstalledAddonsProvider>,
|
|
132
|
+
)
|
|
133
|
+
expect(screen.getByTestId('region').textContent).toBe('')
|
|
134
|
+
let off = () => {}
|
|
135
|
+
act(() => {
|
|
136
|
+
off = registerDocumentContribution({
|
|
137
|
+
id: 'fiscal_mexico.header.cfdi',
|
|
138
|
+
kinds: ['invoice'],
|
|
139
|
+
region: 'header.fields',
|
|
140
|
+
requiresAddon: 'fiscal_mexico',
|
|
141
|
+
load: async () => ({ default: () => <span>Uso del CFDI</span> }),
|
|
142
|
+
})
|
|
143
|
+
registerDocumentContribution({
|
|
144
|
+
id: 'inventory.header.warehouse',
|
|
145
|
+
kinds: '*',
|
|
146
|
+
region: 'header.fields',
|
|
147
|
+
requiresAddon: 'inventory',
|
|
148
|
+
component: () => <span>Almacén</span>,
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
expect(await screen.findByText('Uso del CFDI')).toBeTruthy()
|
|
152
|
+
expect(screen.queryByText('Almacén')).toBeNull()
|
|
153
|
+
act(() => off())
|
|
154
|
+
expect(screen.queryByText('Uso del CFDI')).toBeNull()
|
|
155
|
+
})
|
|
156
|
+
})
|