@controleonline/ui-default 1.0.263

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.
Files changed (56) hide show
  1. package/.github/agents/developer.agent.md +30 -0
  2. package/.github/agents/devops.agent.md +30 -0
  3. package/.github/agents/qa.agent.md +30 -0
  4. package/.github/agents/security.agent.md +30 -0
  5. package/.scrutinizer.yml +20 -0
  6. package/AGENTS.md +47 -0
  7. package/FUNDING.yml +1 -0
  8. package/package.json +21 -0
  9. package/src/react/components/errors/DefaultErrors.js +360 -0
  10. package/src/react/components/files/DefaultFile.js +46 -0
  11. package/src/react/components/filters/CompactFilterSelector.js +262 -0
  12. package/src/react/components/filters/CompactFilterSelector.styles.js +124 -0
  13. package/src/react/components/filters/DateShortcutFilter.js +264 -0
  14. package/src/react/components/filters/DateShortcutFilter.styles.js +82 -0
  15. package/src/react/components/filters/DefaultColumnFilter.js +97 -0
  16. package/src/react/components/filters/DefaultColumnFilter.styles.js +21 -0
  17. package/src/react/components/filters/DefaultExternalFilters.js +441 -0
  18. package/src/react/components/filters/DefaultExternalFilters.styles.js +177 -0
  19. package/src/react/components/filters/DefaultSearch.js +103 -0
  20. package/src/react/components/filters/DefaultSearch.styles.js +70 -0
  21. package/src/react/components/filters/dateFilterSelection.js +29 -0
  22. package/src/react/components/form/DefaultForm.js +198 -0
  23. package/src/react/components/form/DefaultForm.styles.js +70 -0
  24. package/src/react/components/help/DefaultTooltip.js +87 -0
  25. package/src/react/components/help/DefaultTooltip.styles.js +61 -0
  26. package/src/react/components/inputs/DefaultInput.js +160 -0
  27. package/src/react/components/inputs/DefaultInput.styles.js +93 -0
  28. package/src/react/components/inputs/DefaultSelect.js +192 -0
  29. package/src/react/components/inputs/DefaultSelect.styles.js +65 -0
  30. package/src/react/components/inputs/defaultInputUtils.js +230 -0
  31. package/src/react/components/map/DefaultGoogleMap.styles.js +9 -0
  32. package/src/react/components/map/DefaultGoogleMap.web.js +698 -0
  33. package/src/react/components/map/DefaultMap.native.js +71 -0
  34. package/src/react/components/map/DefaultMap.shared.js +762 -0
  35. package/src/react/components/map/DefaultMap.styles.js +16 -0
  36. package/src/react/components/map/DefaultMap.web.js +66 -0
  37. package/src/react/components/map/DefaultNativeMap.native.js +615 -0
  38. package/src/react/components/map/DefaultNativeMap.shared.js +532 -0
  39. package/src/react/components/map/DefaultNativeMap.styles.js +122 -0
  40. package/src/react/components/table/DefaultTable.js +1897 -0
  41. package/src/react/components/table/DefaultTable.styles.js +610 -0
  42. package/src/react/index.js +10 -0
  43. package/src/react/utils/tableVisibleColumnsPreferences.js +264 -0
  44. package/src/store/default/actions.js +444 -0
  45. package/src/store/default/getters.js +28 -0
  46. package/src/store/default/mutation_types.js +26 -0
  47. package/src/store/default/mutations.js +138 -0
  48. package/src/tests/react/components/DateShortcutFilter.test.js +96 -0
  49. package/src/tests/react/components/errors/DefaultErrors.test.js +162 -0
  50. package/src/tests/react/components/files/DefaultFile.test.js +80 -0
  51. package/src/tests/react/components/map/DefaultMap.shared.test.js +162 -0
  52. package/src/tests/react/filters/dateFilterSelection.test.js +46 -0
  53. package/src/tests/react/inputs/defaultInputUtils.test.js +45 -0
  54. package/src/tests/react/store/defaultActions.test.js +112 -0
  55. package/src/tests/react/utils/tableVisibleColumnsPreferences.test.js +223 -0
  56. package/src/utils/filters.js +56 -0
@@ -0,0 +1,28 @@
1
+ export const error = state => state.error;
2
+ export const isLoading = state => state.isLoading;
3
+ export const isSaving = state => state.isSaving;
4
+ export const totalItems = state => state.totalItems;
5
+ export const summary = state => state.summary;
6
+ export const items = state => state.items;
7
+ export const item = state => state.item;
8
+ export const columns = state => state.columns;
9
+ export const filters = state => state.filters;
10
+ export const resourceEndpoint = state => state.resourceEndpoint;
11
+ export const selected = state => state.selected;
12
+ export const visibleColumns = state => state.visibleColumns;
13
+ export const isLoadingList = state => state.isLoadingList;
14
+ export const reload = state => state.reload;
15
+ export const store = state => state.store;
16
+ export const offline = state => state.offline;
17
+ export const payable = state => state.payable;
18
+ export const loadedKey = state => state.loadedKey || '';
19
+ export const loadedAt = state => Number(state.loadedAt) || 0;
20
+ export const print = state => state.print;
21
+ export const message = state => state.message;
22
+ export const messages = state => state.messages;
23
+ export const selections = state => state.selections || {};
24
+ export const selectorModalKey = state => state.selectorModalKey || '';
25
+ export const activeRequestKey = state => state.activeRequestKey || '';
26
+ export const lastCompletedRequest = state => state.lastCompletedRequest || null;
27
+ export const filterId = state => state?.filters?.id || null;
28
+ export const filterDueDate = state => state?.filters?.dueDate || null;
@@ -0,0 +1,26 @@
1
+ export const SET_ERROR = 'SET_ERROR';
2
+ export const SET_ISLOADING = 'SET_ISLOADING';
3
+ export const SET_ISSAVING = 'SET_ISSAVING';
4
+ export const SET_TOTALITEMS = 'SET_TOTALITEMS';
5
+ export const SET_SUMMARY = 'SET_SUMMARY';
6
+ export const SET_ITEMS = 'SET_ITEMS';
7
+ export const SET_ITEM = 'SET_ITEM';
8
+ export const SET_COLUMNS = 'SET_COLUMNS';
9
+ export const SET_FILTERS = 'SET_FILTERS';
10
+ export const SET_RESOURCE_ENDPOINT = 'SET_RESOURCE_ENDPOINT';
11
+ export const SET_SELECTED = 'SET_SELECTED';
12
+ export const SET_VISIBLECOLUMNS = 'SET_VISIBLECOLUMNS';
13
+ export const SET_ISLOADINGLIST = 'SET_ISLOADINGLIST';
14
+ export const SET_RELOAD = 'SET_RELOAD';
15
+ export const SET_STORE = 'SET_STORE';
16
+ export const SET_OFFLINE = 'SET_OFFLINE';
17
+ export const SET_PAYABLE = 'SET_PAYABLE';
18
+ export const SET_LOADED_KEY = 'SET_LOADED_KEY';
19
+ export const SET_LOADED_AT = 'SET_LOADED_AT';
20
+ export const SET_PRINT = 'SET_PRINT';
21
+ export const SET_MESSAGE = 'SET_MESSAGE';
22
+ export const SET_MESSAGES = 'SET_MESSAGES';
23
+ export const SET_SELECTIONS = 'SET_SELECTIONS';
24
+ export const SET_SELECTOR_MODAL_KEY = 'SET_SELECTOR_MODAL_KEY';
25
+ export const SET_ACTIVE_REQUEST_KEY = 'SET_ACTIVE_REQUEST_KEY';
26
+ export const SET_LAST_COMPLETED_REQUEST = 'SET_LAST_COMPLETED_REQUEST';
@@ -0,0 +1,138 @@
1
+ import * as types from './mutation_types';
2
+ import Filters from '@controleonline/ui-default/src/utils/filters';
3
+
4
+ export default {
5
+ [types.SET_ERROR](state, error) {
6
+ state.error = error;
7
+ return 'error';
8
+ },
9
+
10
+ [types.SET_ISLOADING](state, isLoading = true) {
11
+ state.isLoading = isLoading;
12
+ return 'isLoading';
13
+ },
14
+
15
+ [types.SET_ISSAVING](state, isSaving = true) {
16
+ state.isSaving = isSaving;
17
+ return 'isSaving';
18
+ },
19
+
20
+ [types.SET_ISLOADINGLIST](state, isLoadingList = true) {
21
+ state.isLoadingList = isLoadingList;
22
+ return 'isLoadingList';
23
+ },
24
+
25
+ [types.SET_PRINT](state, print) {
26
+ state.print = print;
27
+ return 'print';
28
+ },
29
+
30
+ [types.SET_TOTALITEMS](state, totalItems) {
31
+ state.totalItems = totalItems || 0;
32
+ return 'totalItems';
33
+ },
34
+
35
+ [types.SET_SUMMARY](state, summary) {
36
+ state.summary = summary || {};
37
+ return 'summary';
38
+ },
39
+
40
+ [types.SET_ITEMS](state, items) {
41
+ state.items = items;
42
+ return 'items';
43
+ },
44
+
45
+ [types.SET_ITEM](state, item) {
46
+ state.item = item;
47
+ return 'item';
48
+ },
49
+ [types.SET_MESSAGE](state, message) {
50
+ state.message = message;
51
+ return 'message';
52
+ },
53
+ [types.SET_MESSAGES](state, messages) {
54
+ state.messages = messages;
55
+ return 'messages';
56
+ },
57
+ [types.SET_SELECTIONS](state, selections) {
58
+ state.selections = selections || {};
59
+ return 'selections';
60
+ },
61
+ [types.SET_SELECTOR_MODAL_KEY](state, selectorModalKey) {
62
+ state.selectorModalKey = selectorModalKey || '';
63
+ return 'selectorModalKey';
64
+ },
65
+ [types.SET_ACTIVE_REQUEST_KEY](state, activeRequestKey) {
66
+ state.activeRequestKey = activeRequestKey || '';
67
+ return 'activeRequestKey';
68
+ },
69
+ [types.SET_LAST_COMPLETED_REQUEST](state, lastCompletedRequest) {
70
+ state.lastCompletedRequest = lastCompletedRequest || null;
71
+ return 'lastCompletedRequest';
72
+ },
73
+ [types.SET_COLUMNS](state, columns) {
74
+ state.columns = columns;
75
+ return 'columns';
76
+ },
77
+ [types.SET_OFFLINE](state, offline) {
78
+ state.offline = offline;
79
+ return 'offline';
80
+ },
81
+ [types.SET_STORE](state, store) {
82
+ state.store = store;
83
+ return 'store';
84
+ },
85
+
86
+ [types.SET_FILTERS](state, filters) {
87
+ const routeName = this?.$router?.currentRoute?.value?.name;
88
+ if (routeName) {
89
+ const persistentFilter = new Filters(
90
+ routeName,
91
+ state.store || state.resourceEndpoint,
92
+ );
93
+ persistentFilter.setFilters(filters);
94
+ }
95
+ state.filters = filters;
96
+ return 'filters';
97
+ },
98
+
99
+ [types.SET_RESOURCE_ENDPOINT](state, resourceEndpoint) {
100
+ state.resourceEndpoint = resourceEndpoint;
101
+ return 'resourceEndpoint';
102
+ },
103
+
104
+ [types.SET_SELECTED](state, selected) {
105
+ state.selected = selected;
106
+ return 'selected';
107
+ },
108
+
109
+ [types.SET_PAYABLE](state, payable) {
110
+ state.payable = payable;
111
+ return 'payable';
112
+ },
113
+
114
+ [types.SET_LOADED_KEY](state, loadedKey) {
115
+ state.loadedKey = loadedKey || '';
116
+ return 'loadedKey';
117
+ },
118
+
119
+ [types.SET_LOADED_AT](state, loadedAt) {
120
+ state.loadedAt = Number(loadedAt) || 0;
121
+ return 'loadedAt';
122
+ },
123
+
124
+ [types.SET_RELOAD](state, reload) {
125
+ state.reload = reload;
126
+ return 'reload';
127
+ },
128
+
129
+ [types.SET_VISIBLECOLUMNS](state, visibleColumns) {
130
+ state.visibleColumns = visibleColumns;
131
+ const persistentFilter = new Filters(
132
+ state.$router?.currentRoute?.value?.name,
133
+ state.store || state.resourceEndpoint,
134
+ );
135
+ persistentFilter.setVisibleColumns(visibleColumns);
136
+ return 'visibleColumns';
137
+ },
138
+ };
@@ -0,0 +1,96 @@
1
+ const React = require('react');
2
+ const renderer = require('react-test-renderer');
3
+ const {jest} = require('@jest/globals');
4
+
5
+ const {beforeEach, describe, expect, it} = global;
6
+
7
+ global.IS_REACT_ACT_ENVIRONMENT = true;
8
+
9
+ jest.mock('react-native', () => {
10
+ const React = require('react');
11
+ const createComponent = name => props =>
12
+ React.createElement(name, props, props.children);
13
+
14
+ return {
15
+ StyleSheet: {create: value => value},
16
+ Text: createComponent('Text'),
17
+ TextInput: createComponent('TextInput'),
18
+ TouchableOpacity: createComponent('TouchableOpacity'),
19
+ View: createComponent('View'),
20
+ };
21
+ });
22
+
23
+ jest.mock(
24
+ '../../../react/components/filters/CompactFilterSelector',
25
+ () => props => {
26
+ const React = require('react');
27
+ const content =
28
+ typeof props.children === 'function'
29
+ ? props.children({close: props.onClose || (() => {})})
30
+ : props.children;
31
+
32
+ return React.createElement('CompactFilterSelector', props, content);
33
+ },
34
+ );
35
+
36
+ jest.mock(
37
+ '@controleonline/ui-common/src/react/utils/storeColumns',
38
+ () => ({
39
+ formatStoreColumnLabel: () => 'Periodo',
40
+ resolveStoreColumn: () => ({}),
41
+ resolveStoreConfigByName: () => ({columns: []}),
42
+ }),
43
+ );
44
+
45
+ const DateShortcutFilter =
46
+ require('../../../react/components/filters/DateShortcutFilter').default;
47
+
48
+ describe('DateShortcutFilter', () => {
49
+ beforeEach(() => {
50
+ global.t = {
51
+ t: (_store, group, key) => {
52
+ if (group === 'orders' && key === 'apply_period') return 'Aplicar';
53
+ if (group === 'orders' && key === 'clear') return 'Limpar';
54
+ if (group === 'orders' && key === 'date_from') return 'Data inicial';
55
+ if (group === 'orders' && key === 'date_to') return 'Data final';
56
+ return key;
57
+ },
58
+ };
59
+ });
60
+
61
+ it('keeps the freshly typed custom range when applying it', () => {
62
+ const onChange = jest.fn();
63
+ const onCustomRangeChange = jest.fn();
64
+ let tree;
65
+
66
+ renderer.act(() => {
67
+ tree = renderer.create(
68
+ React.createElement(DateShortcutFilter, {
69
+ customRange: {from: '', to: ''},
70
+ onChange,
71
+ onCustomRangeChange,
72
+ value: 'custom',
73
+ }),
74
+ );
75
+ });
76
+
77
+ const inputs = tree.root.findAllByType('TextInput');
78
+ const buttons = tree.root.findAllByType('TouchableOpacity');
79
+
80
+ renderer.act(() => {
81
+ inputs[0].props.onChangeText('2026-06-01');
82
+ inputs[1].props.onChangeText('2026-06-28');
83
+ });
84
+
85
+ renderer.act(() => {
86
+ buttons[1].props.onPress();
87
+ });
88
+
89
+ expect(onCustomRangeChange).toHaveBeenCalledTimes(1);
90
+ expect(onCustomRangeChange).toHaveBeenCalledWith({
91
+ from: '2026-06-01',
92
+ to: '2026-06-28',
93
+ });
94
+ expect(onChange).not.toHaveBeenCalledWith('custom');
95
+ });
96
+ });
@@ -0,0 +1,162 @@
1
+ const React = require('react');
2
+ const renderer = require('react-test-renderer');
3
+ const {jest} = require('@jest/globals');
4
+
5
+ const {afterEach, beforeEach, describe, expect, it} = global;
6
+
7
+ global.IS_REACT_ACT_ENVIRONMENT = true;
8
+
9
+ let mockStores = {};
10
+ let consoleErrorSpy = null;
11
+
12
+ jest.mock('@store', () => ({
13
+ useStore: jest.fn(name => {
14
+ if (name === 'theme') {
15
+ return {
16
+ getters: {
17
+ colors: {
18
+ inputErrorBackground: '#FEF2F2',
19
+ inputErrorBorder: '#DC2626',
20
+ inputErrorText: '#B91C1C',
21
+ modalBackground: '#FFFFFF',
22
+ modalBorder: '#DC2626',
23
+ modalCloseIcon: '#B91C1C',
24
+ modalOverlay: 'rgba(15, 23, 42, 0.55)',
25
+ modalText: '#334155',
26
+ primary: '#0EA5E9',
27
+ textDanger: '#DC2626',
28
+ },
29
+ },
30
+ };
31
+ }
32
+
33
+ return mockStores[name] || {actions: {}, getters: {}};
34
+ }),
35
+ useStores: jest.fn(selector =>
36
+ (typeof selector === 'function' ? selector(mockStores) : mockStores),
37
+ ),
38
+ }));
39
+
40
+ jest.mock('react-native', () => {
41
+ const React = require('react');
42
+ const createComponent = name => props =>
43
+ React.createElement(name, props, props.children);
44
+
45
+ return {
46
+ Modal: props => React.createElement('modal', props, props.children),
47
+ Pressable: props => React.createElement('pressable', props, props.children),
48
+ StyleSheet: {
49
+ create: styles => styles,
50
+ },
51
+ Text: createComponent('text'),
52
+ TouchableOpacity: createComponent('touchable-opacity'),
53
+ View: createComponent('view'),
54
+ };
55
+ });
56
+
57
+ const DefaultErrors =
58
+ require('../../../../react/components/errors/DefaultErrors').default;
59
+
60
+ const getTextNodes = tree =>
61
+ tree.root
62
+ .findAllByType('text')
63
+ .map(node => (Array.isArray(node.children) ? node.children.join('') : ''))
64
+ .filter(Boolean);
65
+
66
+ describe('DefaultErrors', () => {
67
+ beforeEach(() => {
68
+ jest.useFakeTimers();
69
+ consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
70
+ mockStores = {
71
+ orders: {
72
+ actions: {
73
+ setError: jest.fn(),
74
+ },
75
+ getters: {
76
+ error: 'Falha ao salvar.',
77
+ },
78
+ },
79
+ };
80
+ });
81
+
82
+ afterEach(() => {
83
+ if (consoleErrorSpy) {
84
+ consoleErrorSpy.mockRestore();
85
+ consoleErrorSpy = null;
86
+ }
87
+
88
+ jest.clearAllTimers();
89
+ jest.useRealTimers();
90
+ });
91
+
92
+ it('renders the store error in a popup and clears the store when closed', () => {
93
+ let tree;
94
+
95
+ renderer.act(() => {
96
+ tree = renderer.create(
97
+ React.createElement(DefaultErrors, {
98
+ store: 'orders',
99
+ title: 'Nao foi possivel concluir',
100
+ }),
101
+ );
102
+ });
103
+
104
+ expect(tree.root.findByType('modal').props.visible).toBe(true);
105
+ expect(getTextNodes(tree)).toEqual(
106
+ expect.arrayContaining([
107
+ 'Nao foi possivel concluir',
108
+ 'Falha ao salvar.',
109
+ '×',
110
+ ]),
111
+ );
112
+
113
+ const closeButton = tree.root.findByProps({testID: 'default-errors-close'});
114
+
115
+ renderer.act(() => {
116
+ closeButton.props.onPress();
117
+ });
118
+
119
+ expect(mockStores.orders.actions.setError).toHaveBeenCalledWith('');
120
+ expect(tree.toJSON()).toBeNull();
121
+ });
122
+
123
+ it('renders an explicit popup even without a store binding', () => {
124
+ let tree;
125
+
126
+ renderer.act(() => {
127
+ tree = renderer.create(
128
+ React.createElement(DefaultErrors, {
129
+ title: 'Pedido nao informado.',
130
+ }),
131
+ );
132
+ });
133
+
134
+ expect(tree.root.findByType('modal').props.visible).toBe(true);
135
+ expect(getTextNodes(tree)).toEqual(
136
+ expect.arrayContaining([
137
+ 'Pedido nao informado.',
138
+ '×',
139
+ ]),
140
+ );
141
+ });
142
+
143
+ it('auto closes after five seconds and clears the store error', () => {
144
+ let tree;
145
+
146
+ renderer.act(() => {
147
+ tree = renderer.create(
148
+ React.createElement(DefaultErrors, {
149
+ store: 'orders',
150
+ title: 'Nao foi possivel concluir',
151
+ }),
152
+ );
153
+ });
154
+
155
+ renderer.act(() => {
156
+ jest.advanceTimersByTime(5000);
157
+ });
158
+
159
+ expect(mockStores.orders.actions.setError).toHaveBeenCalledWith('');
160
+ expect(tree.toJSON()).toBeNull();
161
+ });
162
+ });
@@ -0,0 +1,80 @@
1
+ const React = require('react');
2
+ const renderer = require('react-test-renderer');
3
+ const {jest} = require('@jest/globals');
4
+
5
+ const {afterEach, beforeEach, describe, expect, it} = global;
6
+
7
+ global.IS_REACT_ACT_ENVIRONMENT = true;
8
+
9
+ jest.mock('@env', () => ({
10
+ env: {
11
+ API_ENTRYPOINT: 'https://api.controleonline.com',
12
+ DOMAIN: 'manager.controleonline.com',
13
+ },
14
+ }));
15
+
16
+ jest.mock('@store', () => ({
17
+ useStore: jest.fn(() => ({
18
+ getters: {},
19
+ })),
20
+ }));
21
+
22
+ jest.mock('react-native', () => ({
23
+ Image: props => React.createElement('image', props, props.children),
24
+ }));
25
+
26
+ const DefaultFile =
27
+ require('../../../../react/components/files/DefaultFile').default;
28
+
29
+ describe('DefaultFile', () => {
30
+ beforeEach(() => {
31
+ jest.clearAllMocks();
32
+ });
33
+
34
+ afterEach(() => {
35
+ jest.clearAllTimers();
36
+ });
37
+
38
+ it('renders backend download files with app-domain query and headers', () => {
39
+ let tree;
40
+
41
+ renderer.act(() => {
42
+ tree = renderer.create(
43
+ React.createElement(DefaultFile, {
44
+ file: {
45
+ id: 3,
46
+ url: '/files/3/download',
47
+ },
48
+ company: {
49
+ domain: 'maincompany.controleonline.com',
50
+ },
51
+ headers: {
52
+ Authorization: 'Bearer token',
53
+ },
54
+ resizeMode: 'contain',
55
+ }),
56
+ );
57
+ });
58
+
59
+ const image = tree.root.findByType('image');
60
+
61
+ expect(image.props.source).toEqual({
62
+ uri: 'https://api.controleonline.com/files/3/download?app-domain=maincompany.controleonline.com',
63
+ headers: {
64
+ Authorization: 'Bearer token',
65
+ 'app-domain': 'maincompany.controleonline.com',
66
+ },
67
+ });
68
+ expect(image.props.resizeMode).toBe('contain');
69
+ });
70
+
71
+ it('returns null when there is no file source', () => {
72
+ let tree;
73
+
74
+ renderer.act(() => {
75
+ tree = renderer.create(React.createElement(DefaultFile, null));
76
+ });
77
+
78
+ expect(tree.toJSON()).toBeNull();
79
+ });
80
+ });
@@ -0,0 +1,162 @@
1
+ const {
2
+ buildOpenStreetMapEmbedUrl,
3
+ buildOpenStreetMapHtml,
4
+ resolveDefaultMapPayload,
5
+ } = require('../../../../react/components/map/DefaultMap.shared');
6
+
7
+ const {describe, expect, it} = global;
8
+
9
+ describe('DefaultMap.shared', () => {
10
+ it('normalizes addresses into markers, user coordinates and derived paths', () => {
11
+ const payload = resolveDefaultMapPayload({
12
+ addresses: {
13
+ origin: {
14
+ id: 'origin-1',
15
+ latitude: -23.55052,
16
+ longitude: -46.633308,
17
+ title: 'Origem',
18
+ },
19
+ destination: {
20
+ id: 'destination-1',
21
+ latitude: -23.563987,
22
+ longitude: -46.654321,
23
+ title: 'Destino',
24
+ },
25
+ markers: [
26
+ {
27
+ id: 'pickup-1',
28
+ latitude: -23.551,
29
+ longitude: -46.634,
30
+ title: 'Parada extra',
31
+ },
32
+ ],
33
+ user: {
34
+ latitude: -23.549,
35
+ longitude: -46.632,
36
+ },
37
+ },
38
+ });
39
+
40
+ expect(payload.userCoordinates).toEqual({
41
+ latitude: -23.549,
42
+ longitude: -46.632,
43
+ });
44
+ expect(payload.markerPayloads).toHaveLength(3);
45
+ expect(payload.paths).toHaveLength(1);
46
+ expect(payload.paths[0].from.title).toBe('Origem');
47
+ expect(payload.paths[0].to.title).toBe('Destino');
48
+ });
49
+
50
+ it('builds an OpenStreetMap embed URL from the available points', () => {
51
+ const url = buildOpenStreetMapEmbedUrl({
52
+ markerPayloads: [
53
+ {
54
+ id: 'marker-1',
55
+ latitude: -23.55052,
56
+ longitude: -46.633308,
57
+ },
58
+ ],
59
+ paths: [
60
+ {
61
+ id: 'path-1',
62
+ from: {
63
+ latitude: -23.551,
64
+ longitude: -46.634,
65
+ },
66
+ to: {
67
+ latitude: -23.563987,
68
+ longitude: -46.654321,
69
+ },
70
+ },
71
+ ],
72
+ userCoordinates: {
73
+ latitude: -23.549,
74
+ longitude: -46.632,
75
+ },
76
+ });
77
+
78
+ expect(url).toContain('openstreetmap.org/export/embed.html?bbox=');
79
+ expect(url).toContain('layer=mapnik');
80
+ expect(url).toContain('marker=');
81
+ });
82
+
83
+ it('builds an OpenStreetMap html fallback with markers and paths', () => {
84
+ const html = buildOpenStreetMapHtml({
85
+ markerPayloads: [
86
+ {
87
+ id: 'marker-1',
88
+ latitude: -23.55052,
89
+ longitude: -46.633308,
90
+ title: 'Origem',
91
+ markerIconUrl:
92
+ 'https://app.lave-go.com/assets/src/assets/go_google_maps_pin.png',
93
+ },
94
+ ],
95
+ paths: [
96
+ {
97
+ id: 'path-1',
98
+ from: {
99
+ latitude: -23.551,
100
+ longitude: -46.634,
101
+ },
102
+ to: {
103
+ latitude: -23.563987,
104
+ longitude: -46.654321,
105
+ },
106
+ },
107
+ ],
108
+ userCoordinates: {
109
+ latitude: -23.549,
110
+ longitude: -46.632,
111
+ },
112
+ });
113
+
114
+ expect(html).toContain('leaflet');
115
+ expect(html).toContain('window.__SHOP_MAP_MARKERS__');
116
+ expect(html).toContain('Origem');
117
+ expect(html).toContain('markerIconUrl');
118
+ expect(html).toContain('resolveMarkerIcon');
119
+ expect(html).toContain('L.Icon.Default.mergeOptions');
120
+ expect(html).toContain('marker-icon.png');
121
+ expect(html).toContain("item.__mapRole === 'origin'");
122
+ expect(html).toContain("item.__mapRole === 'destination'");
123
+ expect(html).toContain('return L.marker([point.lat, point.lng]).addTo(map);');
124
+ expect(html).toContain('window.__SHOP_MAP_PATHS__');
125
+ expect(html).toContain('router.project-osrm.org');
126
+ });
127
+
128
+ it('resolves nested config addresses and api key from a single JSON payload', () => {
129
+ const payload = resolveDefaultMapPayload({
130
+ config: {
131
+ googleMapsApiKey: 'config-key',
132
+ addresses: {
133
+ origin: {
134
+ id: 'origin-1',
135
+ latitude: -23.55,
136
+ longitude: -46.63,
137
+ nickname: 'Origem',
138
+ },
139
+ markers: [
140
+ {
141
+ id: 'marker-1',
142
+ latitude: -23.56,
143
+ longitude: -46.64,
144
+ nickname: 'Destino',
145
+ },
146
+ ],
147
+ user: {
148
+ latitude: -23.54,
149
+ longitude: -46.62,
150
+ },
151
+ },
152
+ },
153
+ });
154
+
155
+ expect(payload.apiKey).toBe('config-key');
156
+ expect(payload.markerPayloads).toHaveLength(2);
157
+ expect(payload.userCoordinates).toEqual({
158
+ latitude: -23.54,
159
+ longitude: -46.62,
160
+ });
161
+ });
162
+ });