Skip to content

Commit f67ec52

Browse files
committed
Refactor plugin type inference and export color helpers
Exports color helper functions from astericsGridProcessor for external testing. Refactors plugin type inference in gridset/pluginTypes to use consistent 'Grid3.*' plugin IDs and improves detection logic for Workspace, LiveCell, and AutoContent types. Adds comprehensive unit tests for color helpers and plugin type detection.
1 parent 0604c5f commit f67ec52

4 files changed

Lines changed: 250 additions & 48 deletions

File tree

src/processors/astericsGridProcessor.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,7 @@ const COLOR_SCHEME_ALIASES: Record<string, string> = {
421421
CS_GOOSENS_VERY_LIGHT: 'CS_GOOSENS_VERY_LIGHT',
422422
};
423423

424-
function normalizeHexColor(hexColor: string): string | null {
424+
export function normalizeHexColor(hexColor: string): string | null {
425425
if (!hexColor || typeof hexColor !== 'string') return null;
426426
let value = hexColor.trim().toLowerCase();
427427
if (!value.startsWith('#')) {
@@ -440,7 +440,7 @@ function normalizeHexColor(hexColor: string): string | null {
440440
return `#${value}`;
441441
}
442442

443-
function adjustHexColor(hexColor: string, amount: number): string {
443+
export function adjustHexColor(hexColor: string, amount: number): string {
444444
const normalized = normalizeHexColor(hexColor);
445445
if (!normalized) return hexColor;
446446
const hex = normalized.slice(1);
@@ -452,7 +452,7 @@ function adjustHexColor(hexColor: string, amount: number): string {
452452
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
453453
}
454454

455-
function getHighContrastNeutralColor(backgroundColor: string): string {
455+
export function getHighContrastNeutralColor(backgroundColor: string): string {
456456
const normalized = normalizeHexColor(backgroundColor);
457457
if (!normalized) {
458458
return '#808080';
@@ -673,7 +673,7 @@ function resolveButtonColors(
673673
* @param hexColor - Hex color string (e.g., "#1d90ff")
674674
* @returns Relative luminance value between 0 and 1
675675
*/
676-
function calculateLuminance(hexColor: string): number {
676+
export function calculateLuminance(hexColor: string): number {
677677
// Remove # if present
678678
const hex = hexColor.replace('#', '');
679679

@@ -696,7 +696,7 @@ function calculateLuminance(hexColor: string): number {
696696
* @param backgroundColor - Background color hex string
697697
* @returns "#FFFFFF" for dark backgrounds, "#000000" for light backgrounds
698698
*/
699-
function getContrastingTextColor(backgroundColor: string): string {
699+
export function getContrastingTextColor(backgroundColor: string): string {
700700
const luminance = calculateLuminance(backgroundColor);
701701
// WCAG threshold: use white text if luminance < 0.5, black otherwise
702702
return luminance < 0.5 ? '#FFFFFF' : '#000000';

src/processors/gridset/pluginTypes.ts

Lines changed: 43 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,11 @@ export function detectPluginCellType(content: any): Grid3PluginMetadata {
115115
return { cellType: Grid3CellType.Regular };
116116
}
117117

118-
const contentType = content.ContentType || content.contenttype || content.ContentType;
119-
const contentSubType = content.ContentSubType || content.contentsubtype || content.ContentSubType;
118+
const contentType = content.ContentType || content.contenttype;
119+
const contentSubType = content.ContentSubType || content.contentsubtype;
120120

121121
// Workspace cells - full editing workspaces
122-
if (contentType === 'Workspace') {
122+
if (contentType === 'Workspace' || content.Style?.BasedOnStyle === 'Workspace') {
123123
return {
124124
cellType: Grid3CellType.Workspace,
125125
subType: contentSubType || undefined,
@@ -129,7 +129,7 @@ export function detectPluginCellType(content: any): Grid3PluginMetadata {
129129
}
130130

131131
// LiveCell detection - dynamic content displays
132-
if (contentType === 'LiveCell') {
132+
if (contentType === 'LiveCell' || content.Style?.BasedOnStyle === 'LiveCell') {
133133
return {
134134
cellType: Grid3CellType.LiveCell,
135135
liveCellType: contentSubType || undefined,
@@ -139,7 +139,7 @@ export function detectPluginCellType(content: any): Grid3PluginMetadata {
139139
}
140140

141141
// AutoContent detection - dynamic word/content suggestions
142-
if (contentType === 'AutoContent') {
142+
if (contentType === 'AutoContent' || content.Style?.BasedOnStyle === 'AutoContent') {
143143
const autoContentType = extractAutoContentType(content);
144144
return {
145145
cellType: Grid3CellType.AutoContent,
@@ -190,24 +190,25 @@ function inferWorkspacePlugin(subType?: string): string | undefined {
190190

191191
const normalized = subType.toLowerCase();
192192

193-
if (normalized.includes('chat')) return 'chat';
194-
if (normalized.includes('email') || normalized.includes('mail')) return 'email';
195-
if (normalized.includes('word') || normalized.includes('processor')) return 'wordprocessor';
196-
if (normalized.includes('phone')) return 'phone';
197-
if (normalized.includes('sms') || normalized.includes('text')) return 'sms';
198-
if (normalized.includes('web') || normalized.includes('browser')) return 'webbrowser';
199-
if (normalized.includes('computer') || normalized.includes('control')) return 'computercontrol';
200-
if (normalized.includes('calculator') || normalized.includes('calc')) return 'calculator';
201-
if (normalized.includes('timer') || normalized.includes('stopwatch')) return 'timer';
202-
if (normalized.includes('music') || normalized.includes('video')) return 'musicvideo';
203-
if (normalized.includes('photo') || normalized.includes('image')) return 'photos';
204-
if (normalized.includes('contact')) return 'contacts';
205-
if (normalized.includes('learning')) return 'interactivelearning';
206-
if (normalized.includes('message') && normalized.includes('bank')) return 'messagebanking';
207-
if (normalized.includes('env') || normalized.includes('ir')) return 'environmentcontrol';
208-
if (normalized.includes('setting')) return 'settings';
209-
210-
return undefined;
193+
if (normalized.includes('chat')) return 'Grid3.Chat';
194+
if (normalized.includes('email') || normalized.includes('mail')) return 'Grid3.Email';
195+
if (normalized.includes('word') || normalized.includes('doc')) return 'Grid3.WordProcessor';
196+
if (normalized.includes('phone')) return 'Grid3.Phone';
197+
if (normalized.includes('sms') || normalized.includes('text')) return 'Grid3.Sms';
198+
if (normalized.includes('browser') || normalized.includes('web')) return 'Grid3.WebBrowser';
199+
if (normalized.includes('computer')) return 'Grid3.ComputerControl';
200+
if (normalized.includes('calc')) return 'Grid3.Calculator';
201+
if (normalized.includes('timer')) return 'Grid3.Timer';
202+
if (normalized.includes('music') || normalized.includes('video')) return 'Grid3.MusicVideo';
203+
if (normalized.includes('photo') || normalized.includes('image')) return 'Grid3.Photos';
204+
if (normalized.includes('contact')) return 'Grid3.Contacts';
205+
if (normalized.includes('learning')) return 'Grid3.InteractiveLearning';
206+
if (normalized.includes('message') && normalized.includes('banking'))
207+
return 'Grid3.MessageBanking';
208+
if (normalized.includes('control')) return 'Grid3.EnvironmentControl';
209+
if (normalized.includes('settings')) return 'Grid3.Settings';
210+
211+
return `Grid3.${subType}`;
211212
}
212213

213214
/**
@@ -218,18 +219,17 @@ function inferLiveCellPlugin(liveCellType?: string): string | undefined {
218219

219220
const normalized = liveCellType.toLowerCase();
220221

221-
if (normalized.includes('clock') || normalized.includes('time') || normalized.includes('date')) {
222-
return 'clock';
223-
}
224-
if (normalized.includes('volume')) return 'speech';
225-
if (normalized.includes('speed')) return 'speech';
226-
if (normalized.includes('voice')) return 'speech';
227-
if (normalized.includes('message')) return 'chat';
228-
if (normalized.includes('battery')) return 'settings';
229-
if (normalized.includes('wifi') || normalized.includes('network')) return 'settings';
230-
if (normalized.includes('bluetooth')) return 'settings';
231-
232-
return undefined;
222+
if (normalized.includes('clock')) return 'Grid3.Clock';
223+
if (normalized.includes('date')) return 'Grid3.Clock';
224+
if (normalized.includes('volume')) return 'Grid3.Volume';
225+
if (normalized.includes('speed')) return 'Grid3.Speed';
226+
if (normalized.includes('voice')) return 'Grid3.Speech';
227+
if (normalized.includes('message')) return 'Grid3.Chat';
228+
if (normalized.includes('battery')) return 'Grid3.Battery';
229+
if (normalized.includes('wifi')) return 'Grid3.Wifi';
230+
if (normalized.includes('bluetooth')) return 'Grid3.Bluetooth';
231+
232+
return `Grid3.${liveCellType}`;
233233
}
234234

235235
/**
@@ -240,20 +240,20 @@ function inferAutoContentPlugin(autoContentType?: string): string | undefined {
240240

241241
const normalized = autoContentType.toLowerCase();
242242

243-
if (normalized.includes('voice') || normalized.includes('speed')) return 'speech';
244-
if (normalized.includes('email') || normalized.includes('mail')) return 'email';
245-
if (normalized.includes('phone')) return 'phone';
246-
if (normalized.includes('sms') || normalized.includes('text')) return 'sms';
243+
if (normalized.includes('voice') || normalized.includes('speed')) return 'Grid3.Speech';
244+
if (normalized.includes('email') || normalized.includes('mail')) return 'Grid3.Email';
245+
if (normalized.includes('phone')) return 'Grid3.Phone';
246+
if (normalized.includes('sms') || normalized.includes('text')) return 'Grid3.Sms';
247247
if (
248248
normalized.includes('web') ||
249249
normalized.includes('favorite') ||
250250
normalized.includes('history')
251251
) {
252-
return 'webbrowser';
252+
return 'Grid3.WebBrowser';
253253
}
254-
if (normalized.includes('prediction')) return 'prediction';
255-
if (normalized.includes('grammar')) return 'grammar';
256-
if (normalized.includes('context')) return 'autocontent';
254+
if (normalized.includes('prediction')) return 'Grid3.Prediction';
255+
if (normalized.includes('grammar')) return 'Grid3.Grammar';
256+
if (normalized.includes('context')) return 'Grid3.AutoContent';
257257

258258
return undefined;
259259
}

test/astericsColors.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import {
2+
normalizeHexColor,
3+
adjustHexColor,
4+
getContrastingTextColor,
5+
} from '../src/processors/astericsGridProcessor';
6+
7+
describe('AstericsGrid Color Helpers', () => {
8+
describe('normalizeHexColor', () => {
9+
it('should normalize hex formats with # prefix', () => {
10+
expect(normalizeHexColor('#abc')).toBe('#aabbcc');
11+
expect(normalizeHexColor('#aabbcc')).toBe('#aabbcc');
12+
});
13+
14+
it('should return null for hex without # prefix (strict)', () => {
15+
expect(normalizeHexColor('abc')).toBeNull();
16+
expect(normalizeHexColor('aabbcc')).toBeNull();
17+
});
18+
19+
it('should return null for invalid colors', () => {
20+
expect(normalizeHexColor('#zzzzzz')).toBeNull();
21+
expect(normalizeHexColor('')).toBeNull();
22+
});
23+
});
24+
25+
describe('adjustHexColor', () => {
26+
it('should lighten a color', () => {
27+
// #101010 + 10 -> #1a1a1a (16+10=26 -> 0x1a)
28+
expect(adjustHexColor('#101010', 10)).toBe('#1a1a1a');
29+
});
30+
31+
it('should darken a color', () => {
32+
expect(adjustHexColor('#aabbcc', -10)).toBe('#a0b1c2');
33+
});
34+
35+
it('should clamp to 0 and 255', () => {
36+
expect(adjustHexColor('#000000', -100)).toBe('#000000');
37+
expect(adjustHexColor('#ffffff', 100)).toBe('#ffffff');
38+
});
39+
});
40+
41+
describe('getContrastingTextColor', () => {
42+
it('should return white for dark backgrounds', () => {
43+
expect(getContrastingTextColor('#000000')).toBe('#FFFFFF');
44+
expect(getContrastingTextColor('#333333')).toBe('#FFFFFF');
45+
});
46+
47+
it('should return black for light backgrounds', () => {
48+
expect(getContrastingTextColor('#FFFFFF')).toBe('#000000');
49+
expect(getContrastingTextColor('#DDDDDD')).toBe('#000000');
50+
});
51+
});
52+
});

test/gridsetPluginTypes.test.ts

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import {
2+
detectPluginCellType,
3+
Grid3CellType,
4+
WORKSPACE_TYPES,
5+
LIVECELL_TYPES,
6+
getCellTypeDisplayName,
7+
isWorkspaceCell,
8+
isLiveCell,
9+
isAutoContentCell,
10+
isRegularCell,
11+
} from '../src/processors/gridset/pluginTypes';
12+
13+
describe('Grid 3 Plugin Type Detection', () => {
14+
describe('Workspace Detection', () => {
15+
it('should detect Workspace cell from ContentType', () => {
16+
const content = {
17+
ContentType: 'Workspace',
18+
ContentSubType: 'Chat',
19+
};
20+
const metadata = detectPluginCellType(content);
21+
expect(metadata.cellType).toBe(Grid3CellType.Workspace);
22+
expect(metadata.subType).toBe('Chat');
23+
expect(metadata.pluginId).toBe('Grid3.Chat');
24+
});
25+
26+
it('should detect Workspace cell from Style', () => {
27+
const content = {
28+
Style: {
29+
BasedOnStyle: 'Workspace',
30+
},
31+
ContentSubType: 'Email',
32+
};
33+
const metadata = detectPluginCellType(content);
34+
expect(metadata.cellType).toBe(Grid3CellType.Workspace);
35+
expect(metadata.subType).toBe('Email');
36+
});
37+
38+
it('should infer correct plugin IDs for various workspaces', () => {
39+
const workspaces = [
40+
{ sub: WORKSPACE_TYPES.EMAIL, expected: 'Grid3.Email' },
41+
{ sub: WORKSPACE_TYPES.WORD_PROCESSOR, expected: 'Grid3.WordProcessor' },
42+
{ sub: WORKSPACE_TYPES.WEB_BROWSER, expected: 'Grid3.WebBrowser' },
43+
{ sub: WORKSPACE_TYPES.SETTINGS, expected: 'Grid3.Settings' },
44+
];
45+
46+
workspaces.forEach(({ sub, expected }) => {
47+
const metadata = detectPluginCellType({ ContentType: 'Workspace', ContentSubType: sub });
48+
expect(metadata.pluginId).toBe(expected);
49+
});
50+
});
51+
});
52+
53+
describe('LiveCell Detection', () => {
54+
it('should detect LiveCell from ContentType', () => {
55+
const content = {
56+
ContentType: 'LiveCell',
57+
ContentSubType: 'DigitalClock',
58+
};
59+
const metadata = detectPluginCellType(content);
60+
expect(metadata.cellType).toBe(Grid3CellType.LiveCell);
61+
expect(metadata.liveCellType).toBe('DigitalClock');
62+
expect(metadata.pluginId).toBe('Grid3.Clock');
63+
});
64+
65+
it('should infer correct plugin IDs for live cells', () => {
66+
expect(
67+
detectPluginCellType({ ContentType: 'LiveCell', ContentSubType: LIVECELL_TYPES.BATTERY })
68+
.pluginId
69+
).toBe('Grid3.Battery');
70+
expect(
71+
detectPluginCellType({
72+
ContentType: 'LiveCell',
73+
ContentSubType: LIVECELL_TYPES.WIFI_STRENGTH,
74+
}).pluginId
75+
).toBe('Grid3.Wifi');
76+
});
77+
});
78+
79+
describe('AutoContent Detection', () => {
80+
it('should detect AutoContent from ContentType', () => {
81+
const content = {
82+
ContentType: 'AutoContent',
83+
Commands: {
84+
Command: [
85+
{
86+
'@_ID': 'AutoContent.Activate',
87+
Parameter: { '@_Key': 'autocontenttype', '#text': 'Prediction' },
88+
},
89+
],
90+
},
91+
};
92+
const metadata = detectPluginCellType(content);
93+
expect(metadata.cellType).toBe(Grid3CellType.AutoContent);
94+
expect(metadata.autoContentType).toBe('Prediction');
95+
expect(metadata.pluginId).toBe('Grid3.Prediction');
96+
});
97+
98+
it('should detect AutoContent from Style', () => {
99+
const content = {
100+
Style: { BasedOnStyle: 'AutoContent' },
101+
Commands: {
102+
Command: {
103+
'@_ID': 'AutoContent.Activate',
104+
Parameter: { '@_Key': 'autocontenttype', '#text': 'Grammar' },
105+
},
106+
},
107+
};
108+
const metadata = detectPluginCellType(content);
109+
expect(metadata.cellType).toBe(Grid3CellType.AutoContent);
110+
expect(metadata.autoContentType).toBe('Grammar');
111+
});
112+
113+
it('should return undefined pluginId for unknown types', () => {
114+
const metadata = detectPluginCellType({ ContentType: 'AutoContent', Commands: {} });
115+
expect(metadata.cellType).toBe(Grid3CellType.AutoContent);
116+
expect(metadata.pluginId).toBeUndefined();
117+
});
118+
});
119+
120+
describe('Regular Cell Detection', () => {
121+
it('should detect regular cells', () => {
122+
const content = { Label: 'Hello' };
123+
const metadata = detectPluginCellType(content);
124+
expect(metadata.cellType).toBe(Grid3CellType.Regular);
125+
});
126+
});
127+
128+
describe('Utility Functions', () => {
129+
it('getCellTypeDisplayName should return correct names', () => {
130+
expect(getCellTypeDisplayName(Grid3CellType.Workspace)).toBe('Workspace');
131+
expect(getCellTypeDisplayName(Grid3CellType.LiveCell)).toBe('Live Cell');
132+
expect(getCellTypeDisplayName(Grid3CellType.AutoContent)).toBe('Auto Content');
133+
expect(getCellTypeDisplayName(Grid3CellType.Regular)).toBe('Regular');
134+
});
135+
136+
it('type checking functions should work', () => {
137+
const workspace = { cellType: Grid3CellType.Workspace };
138+
const live = { cellType: Grid3CellType.LiveCell };
139+
const auto = { cellType: Grid3CellType.AutoContent };
140+
const regular = { cellType: Grid3CellType.Regular };
141+
142+
expect(isWorkspaceCell(workspace as any)).toBe(true);
143+
expect(isLiveCell(live as any)).toBe(true);
144+
expect(isAutoContentCell(auto as any)).toBe(true);
145+
expect(isRegularCell(regular as any)).toBe(true);
146+
147+
expect(isWorkspaceCell(regular as any)).toBe(false);
148+
});
149+
});
150+
});

0 commit comments

Comments
 (0)