@controleonline/ui-common 1.2.79 → 1.2.81

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 (91) hide show
  1. package/AGENTS.md +8 -0
  2. package/package.json +9 -3
  3. package/src/api/fetch.js +7 -7
  4. package/src/react/components/AddImportModal.js +6 -2
  5. package/src/react/components/BackgroundRuntimeBridge.js +201 -215
  6. package/src/react/components/ConfirmModal.js +44 -9
  7. package/src/react/components/ConfirmModal.styles.js +55 -41
  8. package/src/react/components/CopyDeviceConfigModal.js +193 -0
  9. package/src/react/components/CopyDeviceConfigModal.styles.js +123 -0
  10. package/src/react/components/DefaultProvider.native.js +22 -0
  11. package/src/react/components/DefaultProvider.web.js +25 -1
  12. package/src/react/components/EntityLogBranch.js +482 -0
  13. package/src/react/components/EntityLogCards.js +369 -0
  14. package/src/react/components/EntityLogContent.js +113 -794
  15. package/src/react/components/LinkedOrderProductsTab.js +3 -3
  16. package/src/react/components/PrintService.js +130 -409
  17. package/src/react/components/RemoteCheckoutService.js +2 -2
  18. package/src/react/components/RuntimeBottomNavigationBar.js +141 -146
  19. package/src/react/components/RuntimeFooterMarqueeText.js +137 -0
  20. package/src/react/components/RuntimeInfoFooter.js +12 -15
  21. package/src/react/components/SystemErrorToast.js +49 -29
  22. package/src/react/components/SystemErrorToast.styles.js +43 -38
  23. package/src/react/components/UserAvatar.js +120 -28
  24. package/src/react/config/deviceConfigBootstrap.js +330 -304
  25. package/src/react/hooks/usePrintSpoolEffects.js +161 -0
  26. package/src/react/pages/DeviceDetailPage.js +6 -2882
  27. package/src/react/pages/Devices/detail/DeviceDetailAlertsCommandsSection.js +233 -0
  28. package/src/react/pages/Devices/detail/DeviceDetailHeader.js +138 -0
  29. package/src/react/pages/Devices/detail/DeviceDetailMovementSections.js +167 -0
  30. package/src/react/pages/Devices/detail/DeviceDetailOrdersPrintSection.js +216 -0
  31. package/src/react/pages/Devices/detail/DeviceDetailPaymentSection.js +40 -0
  32. package/src/react/pages/Devices/detail/DeviceDetailPdvConfigSection.js +390 -0
  33. package/src/react/pages/Devices/detail/DeviceDetailRenderers.js +135 -0
  34. package/src/react/pages/Devices/detail/DeviceDetailScreen.js +493 -0
  35. package/src/react/pages/Devices/detail/OptionButtonChip.js +65 -0
  36. package/src/react/pages/Devices/detail/deviceDetailConstants.js +36 -0
  37. package/src/react/pages/Devices/detail/deviceDetailHelpers.js +70 -0
  38. package/src/react/pages/Devices/detail/useDeviceDetailActions.js +483 -0
  39. package/src/react/pages/Devices/detail/useDeviceDetailCopyConfig.js +87 -0
  40. package/src/react/pages/Devices/detail/useDeviceDetailLoaders.js +327 -0
  41. package/src/react/pages/Devices/detail/useDeviceDetailSaves.js +429 -0
  42. package/src/react/pages/Devices/detail/useDeviceDetailStateA.js +484 -0
  43. package/src/react/pages/Devices/detail/useDeviceDetailStateB.js +71 -0
  44. package/src/react/pages/Devices/deviceTypes/index.js +2 -0
  45. package/src/react/pages/Devices/deviceTypes/pdv.js +11 -0
  46. package/src/react/pages/Imports.js +3 -2
  47. package/src/react/pages/IntegrationConfigFields.js +126 -0
  48. package/src/react/pages/IntegrationConfigPage.js +122 -591
  49. package/src/react/pages/IntegrationConfigPage.utils.js +95 -0
  50. package/src/react/print/usePrintButtonController.js +9 -1
  51. package/src/react/services/Getnet/Checkout.js +81 -0
  52. package/src/react/services/Getnet/Getnet.js +77 -0
  53. package/src/react/services/paymentGatewayExecution.js +20 -0
  54. package/src/react/utils/bottomNavigationToolbar.js +161 -0
  55. package/src/react/utils/commercialDocumentOrders.js +18 -6
  56. package/src/react/utils/copyDeviceConfigs.js +198 -0
  57. package/src/react/utils/fileUrl.js +43 -0
  58. package/src/react/utils/importStatus.js +19 -10
  59. package/src/react/utils/logSettings.js +7 -7
  60. package/src/react/utils/maintenanceSettings.js +16 -0
  61. package/src/react/utils/passwordPolicy.js +73 -0
  62. package/src/react/utils/paymentDevices.js +6 -0
  63. package/src/react/utils/printManagedPrinter.js +45 -0
  64. package/src/react/utils/printRouting.js +63 -0
  65. package/src/react/utils/printSpoolUtils.js +119 -0
  66. package/src/react/utils/printerDevices.js +368 -368
  67. package/src/react/utils/runtimeFooter.js +6 -1
  68. package/src/react/utils/shopFranchises.js +186 -65
  69. package/src/react/utils/systemErrorMessage.js +71 -67
  70. package/src/store/device_config/customActions.js +119 -30
  71. package/src/tests/browser/manager/bottom-navigation-role-toolbar.spec.js +353 -0
  72. package/src/tests/browser/manager/device-detail-alias-save.spec.js +345 -0
  73. package/src/tests/browser/manager/device-detail-copy-config.spec.js +365 -0
  74. package/src/tests/browser/manager/runtime-footer-marquee.spec.js +262 -0
  75. package/src/tests/browser/manager/theme-confirm-toast.spec.js +236 -0
  76. package/src/tests/react/components/RuntimeFooterMarqueeText.test.js +171 -0
  77. package/src/tests/react/components/UserAvatar.test.js +6 -2
  78. package/src/tests/react/components/themeAwareModals.test.mjs +60 -0
  79. package/src/tests/react/config/deviceConfigBootstrap.test.js +104 -66
  80. package/src/tests/react/pages/deviceDetailDelete.test.js +78 -0
  81. package/src/tests/react/pages/receitaFederalIntegrationContract.test.mjs +48 -0
  82. package/src/tests/react/utils/bottomNavigationToolbar.test.js +222 -0
  83. package/src/tests/react/utils/copyDeviceConfigs.test.mjs +127 -0
  84. package/src/tests/react/utils/fileUrl.test.js +38 -0
  85. package/src/tests/react/utils/importStatus.test.js +27 -10
  86. package/src/tests/react/utils/passwordPolicy.test.js +27 -0
  87. package/src/tests/react/utils/printRouting.test.mjs +86 -0
  88. package/src/tests/react/utils/printSpoolUtils.test.mjs +52 -0
  89. package/src/tests/react/utils/runtimeFooter.test.js +10 -8
  90. package/src/tests/react/utils/shopFranchises.test.js +121 -115
  91. package/src/tests/react/utils/systemErrorMessage.test.js +32 -59
@@ -0,0 +1,236 @@
1
+ const {expect, test} = require('playwright/test');
2
+ const packageJson = require('../../../../../../../package.json');
3
+ const {API_ORIGIN} = require('../../../../../../../src/tests/browser/apiOrigin');
4
+
5
+ const APP_VERSION = packageJson?.version || '1.0.0';
6
+
7
+ const CORS_HEADERS = {
8
+ 'access-control-allow-origin': '*',
9
+ 'access-control-allow-headers':
10
+ 'API-TOKEN, APP-DOMAIN, DEVICE, ACCEPT, CONTENT-TYPE, X-Requested-With',
11
+ 'access-control-allow-methods': 'GET,POST,PUT,PATCH,DELETE,OPTIONS',
12
+ };
13
+
14
+ const jsonHeaders = () => ({
15
+ ...CORS_HEADERS,
16
+ 'content-type': 'application/ld+json; charset=utf-8',
17
+ });
18
+
19
+ const textHeaders = () => ({
20
+ ...CORS_HEADERS,
21
+ 'content-type': 'text/css; charset=utf-8',
22
+ });
23
+
24
+ const collection = member => ({
25
+ member,
26
+ 'hydra:member': member,
27
+ totalItems: member.length,
28
+ 'hydra:totalItems': member.length,
29
+ summary: {},
30
+ });
31
+
32
+ const createCompany = () => ({
33
+ id: 3,
34
+ name: 'Controle Online',
35
+ alias: 'CONTROLE ONLINE',
36
+ panel_enabled: true,
37
+ enabled: true,
38
+ commercial_enabled: true,
39
+ theme: {
40
+ colors: {
41
+ primary: '#0EA5E9',
42
+ secondary: '#F97316',
43
+ buttonBackground: '#0EA5E9',
44
+ buttonBorder: '#0284C7',
45
+ buttonText: '#FFFFFF',
46
+ buttonTextSecondary: '#64748B',
47
+ cardBackground: '#FFFFFF',
48
+ cardBorder: '#D8E0EA',
49
+ modalOverlay: 'rgba(15, 23, 42, 0.45)',
50
+ modalBackground: '#FFFFFF',
51
+ modalBorder: '#D8E0EA',
52
+ modalHeaderText: '#0F172A',
53
+ modalText: '#334155',
54
+ toastDangerBackground: '#FEE2E2',
55
+ toastDangerBorder: '#FECACA',
56
+ toastDangerIcon: '#DC2626',
57
+ toastDangerText: '#991B1B',
58
+ },
59
+ },
60
+ configs: {},
61
+ });
62
+
63
+ const mockApi = async page => {
64
+ const company = createCompany();
65
+
66
+ await page.route(`${API_ORIGIN}/**`, async route => {
67
+ const request = route.request();
68
+ const url = new URL(request.url());
69
+ const pathname = url.pathname.replace(/^\/+/, '');
70
+ const method = request.method().toUpperCase();
71
+
72
+ if (method === 'OPTIONS') {
73
+ return route.fulfill({status: 204, headers: CORS_HEADERS, body: ''});
74
+ }
75
+
76
+ if (pathname === 'themes-colors.css') {
77
+ return route.fulfill({
78
+ status: 200,
79
+ headers: textHeaders(),
80
+ body: ':root { --primary: #0ea5e9; --modal-overlay: rgba(15,23,42,0.45); }',
81
+ });
82
+ }
83
+
84
+ if (pathname === 'runtime/ip') {
85
+ return route.fulfill({
86
+ status: 200,
87
+ headers: jsonHeaders(),
88
+ body: JSON.stringify({ip: '127.0.0.1'}),
89
+ });
90
+ }
91
+
92
+ if (
93
+ pathname === 'people/company/default' ||
94
+ pathname === 'people' ||
95
+ pathname === 'devices' ||
96
+ pathname === 'device_configs' ||
97
+ pathname === 'menus-people'
98
+ ) {
99
+ return route.fulfill({
100
+ status: 200,
101
+ headers: jsonHeaders(),
102
+ body: JSON.stringify(collection([company])),
103
+ });
104
+ }
105
+
106
+ return route.fulfill({
107
+ status: 200,
108
+ headers: jsonHeaders(),
109
+ body: JSON.stringify(collection([])),
110
+ });
111
+ });
112
+
113
+ await page.addInitScript(
114
+ ({appVersion}) => {
115
+ window.localStorage.setItem(
116
+ 'session',
117
+ JSON.stringify({
118
+ id: 7,
119
+ people: '/people/7',
120
+ api_key: 'test-api-key',
121
+ active: 1,
122
+ mycompany: 3,
123
+ roles: ['ROLE_SUPER'],
124
+ phone: '(11) 97114-0832',
125
+ email: 'qa@controleonline.com',
126
+ }),
127
+ );
128
+ window.localStorage.setItem('config', JSON.stringify({language: 'pt-br'}));
129
+ window.localStorage.setItem('app-type', 'MANAGER');
130
+ window.localStorage.setItem(
131
+ 'device',
132
+ JSON.stringify({
133
+ id: 'web-manager',
134
+ device: 'web-manager',
135
+ type: 'WEB',
136
+ appName: 'Browser Manager',
137
+ appVersion,
138
+ buildNumber: appVersion,
139
+ systemName: 'web',
140
+ systemVersion: 'web',
141
+ deviceType: 'web',
142
+ metadata: {},
143
+ }),
144
+ );
145
+ },
146
+ {appVersion: APP_VERSION},
147
+ );
148
+ };
149
+
150
+ test.describe('theme ConfirmModal + SystemErrorToast (#362)', () => {
151
+ test('manager shell loads with theme tokens and no console errors on shared UI', async ({
152
+ page,
153
+ }) => {
154
+ const consoleErrors = [];
155
+ page.on('console', message => {
156
+ if (message.type() === 'error') {
157
+ consoleErrors.push(message.text());
158
+ }
159
+ });
160
+ page.on('pageerror', error => {
161
+ consoleErrors.push(String(error?.message || error));
162
+ });
163
+
164
+ await mockApi(page);
165
+ await page.goto('/devices-index?store=device_config');
166
+
167
+ // Page should settle without theme/runtime crashes
168
+ await page.waitForTimeout(2000);
169
+
170
+ // Soft presence check — devices index or empty state or nav shell
171
+ const bodyText = await page.locator('body').innerText();
172
+ expect(bodyText.length).toBeGreaterThan(0);
173
+
174
+ // No React / theme token resolution crashes
175
+ const relevant = consoleErrors.filter(
176
+ text =>
177
+ /theme|TypeError|Cannot read|undefined is not|StyleSheet|modalOverlay|toastDanger/i.test(
178
+ text,
179
+ ),
180
+ );
181
+ expect(
182
+ relevant,
183
+ `Unexpected theme/runtime console errors: ${JSON.stringify(relevant)}`,
184
+ ).toEqual([]);
185
+ });
186
+
187
+ test('SystemErrorToast path accepts theme palette without throwing when error is published', async ({
188
+ page,
189
+ }) => {
190
+ const consoleErrors = [];
191
+ page.on('console', message => {
192
+ if (message.type() === 'error') {
193
+ consoleErrors.push(message.text());
194
+ }
195
+ });
196
+ page.on('pageerror', error => {
197
+ consoleErrors.push(String(error?.message || error));
198
+ });
199
+
200
+ await mockApi(page);
201
+ await page.goto('/devices-index?store=device_config');
202
+ await page.waitForTimeout(1500);
203
+
204
+ // Attempt to publish a system error through any global channel the app may expose
205
+ await page.evaluate(() => {
206
+ try {
207
+ // MessageService / systemErrorChannel may be reachable via window hooks in some builds
208
+ if (typeof window.__publishSystemError === 'function') {
209
+ window.__publishSystemError('QA theme toast probe #362');
210
+ return;
211
+ }
212
+ // Fallback: dispatch a CustomEvent some runtimes listen to
213
+ window.dispatchEvent(
214
+ new CustomEvent('controleonline:system-error', {
215
+ detail: {message: 'QA theme toast probe #362'},
216
+ }),
217
+ );
218
+ } catch (e) {
219
+ // Swallow — presence of throw is asserted via consoleErrors
220
+ console.error(String(e));
221
+ }
222
+ });
223
+
224
+ await page.waitForTimeout(800);
225
+
226
+ const relevant = consoleErrors.filter(text =>
227
+ /theme|modalOverlay|toastDanger|Cannot read properties of undefined \(reading 'colors'\)/i.test(
228
+ text,
229
+ ),
230
+ );
231
+ expect(
232
+ relevant,
233
+ `Theme token errors after system-error probe: ${JSON.stringify(relevant)}`,
234
+ ).toEqual([]);
235
+ });
236
+ });
@@ -0,0 +1,171 @@
1
+ const assert = require('node:assert/strict');
2
+ const {test, before, after, describe} = require('node:test');
3
+ const Module = require('module');
4
+ const path = require('path');
5
+ const React = require('react');
6
+
7
+ const originalLoad = Module._load;
8
+ let lastLoopConfig = null;
9
+ let animationStarts = 0;
10
+
11
+ before(() => {
12
+ Module._load = function mockLoad(request, parent, isMain) {
13
+ if (request === 'react-native') {
14
+ function AnimatedValue(v) {
15
+ this._value = v;
16
+ }
17
+ AnimatedValue.prototype.setValue = function (v) {
18
+ this._value = v;
19
+ };
20
+ AnimatedValue.prototype.stopAnimation = function () {};
21
+
22
+ const timing = (value, config) => ({
23
+ _type: 'timing',
24
+ value,
25
+ config,
26
+ start: cb => {
27
+ animationStarts += 1;
28
+ if (typeof cb === 'function') cb({finished: true});
29
+ },
30
+ stop: () => {},
31
+ });
32
+
33
+ return {
34
+ Animated: {
35
+ Value: AnimatedValue,
36
+ View: props =>
37
+ React.createElement('AnimatedView', props, props.children),
38
+ Text: props =>
39
+ React.createElement('AnimatedText', props, props.children),
40
+ timing,
41
+ sequence: items => ({
42
+ _type: 'sequence',
43
+ items,
44
+ start: cb => {
45
+ animationStarts += 1;
46
+ if (typeof cb === 'function') cb({finished: true});
47
+ },
48
+ stop: () => {},
49
+ }),
50
+ loop: anim => {
51
+ lastLoopConfig = anim;
52
+ return {
53
+ _type: 'loop',
54
+ anim,
55
+ start: cb => {
56
+ animationStarts += 1;
57
+ if (typeof cb === 'function') cb({finished: true});
58
+ },
59
+ stop: () => {},
60
+ };
61
+ },
62
+ delay: ms => ({_type: 'delay', ms, start: () => {}, stop: () => {}}),
63
+ },
64
+ Text: props => React.createElement('Text', props, props.children),
65
+ View: props => React.createElement('View', props, props.children),
66
+ StyleSheet: {create: styles => styles},
67
+ };
68
+ }
69
+ return originalLoad.call(this, request, parent, isMain);
70
+ };
71
+ });
72
+
73
+ after(() => {
74
+ Module._load = originalLoad;
75
+ });
76
+
77
+ const loadComponent = () => {
78
+ const componentPath = path.resolve(
79
+ __dirname,
80
+ '../../../react/components/RuntimeFooterMarqueeText.js',
81
+ );
82
+ delete require.cache[componentPath];
83
+ return require(componentPath);
84
+ };
85
+
86
+ test('RuntimeFooterMarqueeText exports a React component', () => {
87
+ const mod = loadComponent();
88
+ assert.equal(typeof mod, 'function');
89
+ assert.equal(typeof mod.default, 'function');
90
+ });
91
+
92
+ test('short text stays single-line without duplicate marquee copy', async () => {
93
+ const RuntimeFooterMarqueeText = loadComponent();
94
+ const renderer = require('react-test-renderer');
95
+
96
+ animationStarts = 0;
97
+ lastLoopConfig = null;
98
+
99
+ let tree;
100
+ renderer.act(() => {
101
+ tree = renderer.create(
102
+ React.createElement(RuntimeFooterMarqueeText, {
103
+ text: 'v1.2.3 • PDV',
104
+ color: '#111',
105
+ style: {fontSize: 10},
106
+ }),
107
+ );
108
+ });
109
+
110
+ const root = tree.root.findByProps({testID: 'runtime-footer-marquee-text'});
111
+ renderer.act(() => {
112
+ root.props.onLayout({nativeEvent: {layout: {width: 400, height: 12}}});
113
+ });
114
+
115
+ const textNodes = tree.root.findAllByType('Text');
116
+ assert.equal(textNodes.length, 1);
117
+ assert.equal(textNodes[0].props.numberOfLines, 1);
118
+ assert.equal(textNodes[0].props.children, 'v1.2.3 • PDV');
119
+
120
+ renderer.act(() => {
121
+ textNodes[0].props.onLayout({
122
+ nativeEvent: {layout: {width: 80, height: 12}},
123
+ });
124
+ });
125
+
126
+ // Still short: content 80 < container 400 → no duplicate
127
+ assert.equal(tree.root.findAllByType('Text').length, 1);
128
+ assert.equal(lastLoopConfig, null);
129
+ });
130
+
131
+ test('long text enables marquee duplicate and starts loop animation', async () => {
132
+ const RuntimeFooterMarqueeText = loadComponent();
133
+ const renderer = require('react-test-renderer');
134
+
135
+ animationStarts = 0;
136
+ lastLoopConfig = null;
137
+
138
+ const longText =
139
+ 'Mensagem muito longa do rodape runtime para validar deslizamento horizontal continuo no Manager e PDV';
140
+
141
+ let tree;
142
+ renderer.act(() => {
143
+ tree = renderer.create(
144
+ React.createElement(RuntimeFooterMarqueeText, {
145
+ text: longText,
146
+ color: '#222',
147
+ style: {fontSize: 10},
148
+ }),
149
+ );
150
+ });
151
+
152
+ const root = tree.root.findByProps({testID: 'runtime-footer-marquee-text'});
153
+ renderer.act(() => {
154
+ root.props.onLayout({nativeEvent: {layout: {width: 120, height: 12}}});
155
+ });
156
+
157
+ const firstText = tree.root.findAllByType('Text')[0];
158
+ renderer.act(() => {
159
+ firstText.props.onLayout({
160
+ nativeEvent: {layout: {width: 480, height: 12}},
161
+ });
162
+ });
163
+
164
+ const texts = tree.root.findAllByType('Text');
165
+ assert.equal(texts.length, 2, 'duplicate copy for seamless loop');
166
+ assert.equal(texts[0].props.children, longText);
167
+ assert.equal(texts[1].props.children, longText);
168
+ assert.equal(texts[0].props.numberOfLines, 1);
169
+ assert.ok(lastLoopConfig, 'Animated.loop should start when overflowing');
170
+ assert.ok(animationStarts >= 1);
171
+ });
@@ -4,6 +4,7 @@ const {jest} = require('@jest/globals');
4
4
 
5
5
  jest.mock('react-native', () => ({
6
6
  Image: props => React.createElement('Image', props),
7
+ Platform: {OS: 'ios'},
7
8
  StyleSheet: {
8
9
  create: styles => styles,
9
10
  },
@@ -33,20 +34,23 @@ describe('UserAvatar', () => {
33
34
  expect(tree.root.findByType('Text').props.children).toBe('CT');
34
35
  });
35
36
 
36
- it('uses gravatar as fallback when enabled', () => {
37
+ it('uses gravatar as fallback when enabled (native path)', async () => {
37
38
  let tree;
38
39
 
39
- renderer.act(() => {
40
+ await renderer.act(async () => {
40
41
  tree = renderer.create(
41
42
  React.createElement(UserAvatar, {
42
43
  email: 'client@example.com',
43
44
  name: 'Client Test',
44
45
  }),
45
46
  );
47
+ // flush useEffect
48
+ await Promise.resolve();
46
49
  });
47
50
 
48
51
  const image = tree.root.findByType('Image');
49
52
 
50
53
  expect(image.props.source.uri).toContain('gravatar.com/avatar/');
54
+ expect(image.props.source.uri).toContain('d=404');
51
55
  });
52
56
  });
@@ -0,0 +1,60 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {readFileSync} from 'node:fs';
4
+ import {fileURLToPath} from 'node:url';
5
+ import {dirname, join} from 'node:path';
6
+
7
+ const root = join(dirname(fileURLToPath(import.meta.url)), '../../..');
8
+
9
+ const FORBIDDEN_COLOR = /#[0-9a-fA-F]{3,8}\b|\brgba?\s*\(|\bwhite\b|\bblack\b/i;
10
+
11
+ function loadSource(rel) {
12
+ return readFileSync(join(root, rel), 'utf8');
13
+ }
14
+
15
+ test('ConfirmModal.js sources colors only from themeStore getters.colors', () => {
16
+ const src = loadSource('react/components/ConfirmModal.js');
17
+ assert.match(src, /themeStore\.getters\.colors|themeColors/);
18
+ assert.match(src, /modalOverlay|modalBackground|buttonBackground/);
19
+ assert.doesNotMatch(
20
+ src.replace(/transparent/g, ''),
21
+ FORBIDDEN_COLOR,
22
+ 'ConfirmModal.js must not hardcode colors',
23
+ );
24
+ });
25
+
26
+ test('ConfirmModal.styles.js createStyles uses only palette tokens', () => {
27
+ const src = loadSource('react/components/ConfirmModal.styles.js');
28
+ assert.match(src, /palette\.modalOverlay/);
29
+ assert.match(src, /palette\.modalBackground/);
30
+ assert.match(src, /palette\.buttonBackground/);
31
+ assert.match(src, /palette\.buttonText/);
32
+ // strip comments and the word transparent (Modal prop, not a color literal in styles)
33
+ const stripped = src
34
+ .replace(/\/\*[\s\S]*?\*\//g, '')
35
+ .replace(/\/\/.*$/gm, '')
36
+ .replace(/transparent/gi, '');
37
+ assert.doesNotMatch(stripped, FORBIDDEN_COLOR);
38
+ });
39
+
40
+ test('SystemErrorToast.js sources colors only from themeStore getters.colors', () => {
41
+ const src = loadSource('react/components/SystemErrorToast.js');
42
+ assert.match(src, /themeStore\.getters\.colors|themeColors/);
43
+ assert.match(src, /toastDangerBackground|toastDangerText/);
44
+ assert.doesNotMatch(
45
+ src.replace(/transparent/g, ''),
46
+ FORBIDDEN_COLOR,
47
+ 'SystemErrorToast.js must not hardcode colors',
48
+ );
49
+ });
50
+
51
+ test('SystemErrorToast.styles.js createStyles uses only palette tokens', () => {
52
+ const src = loadSource('react/components/SystemErrorToast.styles.js');
53
+ assert.match(src, /palette\.toastDangerBackground/);
54
+ assert.match(src, /palette\.toastDangerText/);
55
+ const stripped = src
56
+ .replace(/\/\*[\s\S]*?\*\//g, '')
57
+ .replace(/\/\/.*$/gm, '')
58
+ .replace(/transparent/gi, '');
59
+ assert.doesNotMatch(stripped, FORBIDDEN_COLOR);
60
+ });