meno-core 1.0.19 → 1.0.21
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/.claude/settings.local.json +7 -0
- package/lib/client/core/ComponentBuilder.test.ts +68 -56
- package/lib/client/core/ComponentBuilder.ts +6 -4
- package/lib/client/core/builders/embedBuilder.ts +10 -1
- package/lib/client/core/builders/index.ts +6 -2
- package/lib/client/core/builders/{cmsListBuilder.ts → listBuilder.ts} +227 -95
- package/lib/client/responsiveStyleResolver.test.ts +12 -12
- package/lib/client/responsiveStyleResolver.ts +19 -7
- package/lib/client/routing/Router.tsx +35 -7
- package/lib/client/templateEngine.test.ts +126 -0
- package/lib/client/templateEngine.ts +53 -13
- package/lib/server/jsonLoader.test.ts +4 -1
- package/lib/server/jsonLoader.ts +64 -15
- package/lib/server/services/configService.ts +68 -13
- package/lib/server/ssr/attributeBuilder.ts +8 -0
- package/lib/server/ssr/index.ts +1 -1
- package/lib/server/ssr/ssrRenderer.ts +245 -111
- package/lib/server/ssrRenderer.test.ts +197 -3
- package/lib/server/validateStyleCoverage.ts +14 -17
- package/lib/shared/breakpoints.test.ts +210 -23
- package/lib/shared/breakpoints.ts +124 -17
- package/lib/shared/constants.test.ts +1 -1
- package/lib/shared/constants.ts +5 -1
- package/lib/shared/cssGeneration.test.ts +17 -0
- package/lib/shared/cssGeneration.ts +49 -12
- package/lib/shared/index.ts +3 -0
- package/lib/shared/itemTemplateUtils.test.ts +44 -2
- package/lib/shared/itemTemplateUtils.ts +15 -2
- package/lib/shared/nodeUtils.ts +23 -4
- package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +2 -2
- package/lib/shared/registry/nodeTypes/ListNodeType.ts +186 -0
- package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +6 -0
- package/lib/shared/registry/nodeTypes/index.ts +6 -5
- package/lib/shared/responsiveScaling.test.ts +87 -0
- package/lib/shared/responsiveScaling.ts +33 -29
- package/lib/shared/responsiveStyleUtils.test.ts +7 -7
- package/lib/shared/responsiveStyleUtils.ts +22 -16
- package/lib/shared/styleNodeUtils.ts +5 -5
- package/lib/shared/styleValueRegistry.ts +60 -5
- package/lib/shared/tree/PathBuilder.ts +3 -3
- package/lib/shared/treePathUtils.ts +7 -5
- package/lib/shared/types/cms.ts +4 -57
- package/lib/shared/types/components.ts +45 -4
- package/lib/shared/types/index.ts +13 -0
- package/lib/shared/utilityClassConfig.ts +14 -0
- package/lib/shared/utilityClassMapper.ts +43 -2
- package/lib/shared/validation/propValidator.ts +9 -1
- package/lib/shared/validation/schemas.ts +60 -14
- package/package.json +1 -1
- package/lib/shared/registry/nodeTypes/CMSListNodeType.ts +0 -109
|
@@ -1214,8 +1214,9 @@ describe("SSR Renderer - CMSList rendering", () => {
|
|
|
1214
1214
|
|
|
1215
1215
|
const pageData: JSONPage = {
|
|
1216
1216
|
root: {
|
|
1217
|
-
type: "
|
|
1218
|
-
|
|
1217
|
+
type: "list",
|
|
1218
|
+
sourceType: "collection",
|
|
1219
|
+
source: "blog-posts",
|
|
1219
1220
|
children: []
|
|
1220
1221
|
} as unknown as ComponentNode
|
|
1221
1222
|
};
|
|
@@ -1224,7 +1225,7 @@ describe("SSR Renderer - CMSList rendering", () => {
|
|
|
1224
1225
|
const result = await renderPageSSR(pageData, {}, '/', '');
|
|
1225
1226
|
|
|
1226
1227
|
expect(result.html).toBe('');
|
|
1227
|
-
expect(consoleSpy).toHaveBeenCalledWith('
|
|
1228
|
+
expect(consoleSpy).toHaveBeenCalledWith('List with sourceType "collection" requires CMS service');
|
|
1228
1229
|
|
|
1229
1230
|
consoleSpy.mockRestore();
|
|
1230
1231
|
});
|
|
@@ -1918,3 +1919,196 @@ describe("SSR Renderer - Interactive Styles", () => {
|
|
|
1918
1919
|
});
|
|
1919
1920
|
});
|
|
1920
1921
|
});
|
|
1922
|
+
|
|
1923
|
+
describe("SSR Renderer - List node rendering", () => {
|
|
1924
|
+
test("should render children for each item in list prop", async () => {
|
|
1925
|
+
// Component with list prop definition
|
|
1926
|
+
const components: Record<string, ComponentDefinition> = {
|
|
1927
|
+
ItemsList: {
|
|
1928
|
+
component: {
|
|
1929
|
+
interface: {
|
|
1930
|
+
items: {
|
|
1931
|
+
type: 'list',
|
|
1932
|
+
itemSchema: {
|
|
1933
|
+
title: { type: 'string' }
|
|
1934
|
+
},
|
|
1935
|
+
default: []
|
|
1936
|
+
}
|
|
1937
|
+
},
|
|
1938
|
+
structure: {
|
|
1939
|
+
type: "list",
|
|
1940
|
+
source: "items",
|
|
1941
|
+
children: [
|
|
1942
|
+
{ type: "node", tag: "div", children: ["{{item.title}}"] }
|
|
1943
|
+
]
|
|
1944
|
+
} as any
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
};
|
|
1948
|
+
|
|
1949
|
+
const pageData: JSONPage = {
|
|
1950
|
+
root: {
|
|
1951
|
+
type: "component",
|
|
1952
|
+
component: "ItemsList",
|
|
1953
|
+
props: {
|
|
1954
|
+
items: [
|
|
1955
|
+
{ title: "First Item" },
|
|
1956
|
+
{ title: "Second Item" },
|
|
1957
|
+
{ title: "Third Item" }
|
|
1958
|
+
]
|
|
1959
|
+
}
|
|
1960
|
+
} as any
|
|
1961
|
+
};
|
|
1962
|
+
|
|
1963
|
+
const result = await renderPageSSR(pageData, components, '/', '');
|
|
1964
|
+
|
|
1965
|
+
expect(result.html).toContain('First Item');
|
|
1966
|
+
expect(result.html).toContain('Second Item');
|
|
1967
|
+
expect(result.html).toContain('Third Item');
|
|
1968
|
+
});
|
|
1969
|
+
|
|
1970
|
+
test("should provide item context variables (itemIndex, itemFirst, itemLast)", async () => {
|
|
1971
|
+
const components: Record<string, ComponentDefinition> = {
|
|
1972
|
+
TagsList: {
|
|
1973
|
+
component: {
|
|
1974
|
+
interface: {
|
|
1975
|
+
tags: {
|
|
1976
|
+
type: 'list',
|
|
1977
|
+
itemSchema: {
|
|
1978
|
+
name: { type: 'string' }
|
|
1979
|
+
},
|
|
1980
|
+
default: []
|
|
1981
|
+
}
|
|
1982
|
+
},
|
|
1983
|
+
structure: {
|
|
1984
|
+
type: "list",
|
|
1985
|
+
source: "tags",
|
|
1986
|
+
itemAs: "tag",
|
|
1987
|
+
children: [
|
|
1988
|
+
{ type: "node", tag: "span", attributes: { 'data-index': '{{tagIndex}}', 'data-first': '{{tagFirst}}', 'data-last': '{{tagLast}}' }, children: ["{{tag.name}}"] }
|
|
1989
|
+
]
|
|
1990
|
+
} as any
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
};
|
|
1994
|
+
|
|
1995
|
+
const pageData: JSONPage = {
|
|
1996
|
+
root: {
|
|
1997
|
+
type: "component",
|
|
1998
|
+
component: "TagsList",
|
|
1999
|
+
props: {
|
|
2000
|
+
tags: [
|
|
2001
|
+
{ name: "Tag1" },
|
|
2002
|
+
{ name: "Tag2" }
|
|
2003
|
+
]
|
|
2004
|
+
}
|
|
2005
|
+
} as any
|
|
2006
|
+
};
|
|
2007
|
+
|
|
2008
|
+
const result = await renderPageSSR(pageData, components, '/', '');
|
|
2009
|
+
|
|
2010
|
+
expect(result.html).toContain('data-index="0"');
|
|
2011
|
+
expect(result.html).toContain('data-index="1"');
|
|
2012
|
+
expect(result.html).toContain('data-first="true"');
|
|
2013
|
+
expect(result.html).toContain('data-last="true"');
|
|
2014
|
+
expect(result.html).toContain('Tag1');
|
|
2015
|
+
expect(result.html).toContain('Tag2');
|
|
2016
|
+
});
|
|
2017
|
+
|
|
2018
|
+
test("should render empty when list prop is empty", async () => {
|
|
2019
|
+
const components: Record<string, ComponentDefinition> = {
|
|
2020
|
+
EmptyList: {
|
|
2021
|
+
component: {
|
|
2022
|
+
interface: {
|
|
2023
|
+
items: {
|
|
2024
|
+
type: 'list',
|
|
2025
|
+
itemSchema: {
|
|
2026
|
+
title: { type: 'string' }
|
|
2027
|
+
},
|
|
2028
|
+
default: []
|
|
2029
|
+
}
|
|
2030
|
+
},
|
|
2031
|
+
structure: {
|
|
2032
|
+
type: "list",
|
|
2033
|
+
source: "items",
|
|
2034
|
+
children: [
|
|
2035
|
+
{ type: "node", tag: "div", children: ["{{item.title}}"] }
|
|
2036
|
+
]
|
|
2037
|
+
} as any
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
};
|
|
2041
|
+
|
|
2042
|
+
const pageData: JSONPage = {
|
|
2043
|
+
root: {
|
|
2044
|
+
type: "component",
|
|
2045
|
+
component: "EmptyList",
|
|
2046
|
+
props: {
|
|
2047
|
+
items: []
|
|
2048
|
+
}
|
|
2049
|
+
} as any
|
|
2050
|
+
};
|
|
2051
|
+
|
|
2052
|
+
const result = await renderPageSSR(pageData, components, '/', '');
|
|
2053
|
+
|
|
2054
|
+
// Should not render any list items
|
|
2055
|
+
expect(result.html).not.toContain('item.');
|
|
2056
|
+
});
|
|
2057
|
+
|
|
2058
|
+
test("should support nested lists", async () => {
|
|
2059
|
+
const components: Record<string, ComponentDefinition> = {
|
|
2060
|
+
CategoryList: {
|
|
2061
|
+
component: {
|
|
2062
|
+
interface: {
|
|
2063
|
+
categories: {
|
|
2064
|
+
type: 'list',
|
|
2065
|
+
itemSchema: {
|
|
2066
|
+
name: { type: 'string' }
|
|
2067
|
+
},
|
|
2068
|
+
default: []
|
|
2069
|
+
}
|
|
2070
|
+
},
|
|
2071
|
+
structure: {
|
|
2072
|
+
type: "list",
|
|
2073
|
+
source: "categories",
|
|
2074
|
+
itemAs: "category",
|
|
2075
|
+
children: [
|
|
2076
|
+
{ type: "node", tag: "div", children: [
|
|
2077
|
+
"{{category.name}}",
|
|
2078
|
+
{
|
|
2079
|
+
type: "list",
|
|
2080
|
+
source: "{{category.items}}",
|
|
2081
|
+
itemAs: "subItem",
|
|
2082
|
+
children: [
|
|
2083
|
+
{ type: "node", tag: "span", children: ["{{subItem.label}}"] }
|
|
2084
|
+
]
|
|
2085
|
+
}
|
|
2086
|
+
] }
|
|
2087
|
+
]
|
|
2088
|
+
} as any
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
};
|
|
2092
|
+
|
|
2093
|
+
const pageData: JSONPage = {
|
|
2094
|
+
root: {
|
|
2095
|
+
type: "component",
|
|
2096
|
+
component: "CategoryList",
|
|
2097
|
+
props: {
|
|
2098
|
+
categories: [
|
|
2099
|
+
{ name: "Category A", items: [{ label: "Item A1" }, { label: "Item A2" }] },
|
|
2100
|
+
{ name: "Category B", items: [{ label: "Item B1" }] }
|
|
2101
|
+
]
|
|
2102
|
+
}
|
|
2103
|
+
} as any
|
|
2104
|
+
};
|
|
2105
|
+
|
|
2106
|
+
const result = await renderPageSSR(pageData, components, '/', '');
|
|
2107
|
+
|
|
2108
|
+
expect(result.html).toContain('Category A');
|
|
2109
|
+
expect(result.html).toContain('Category B');
|
|
2110
|
+
expect(result.html).toContain('Item A1');
|
|
2111
|
+
expect(result.html).toContain('Item A2');
|
|
2112
|
+
expect(result.html).toContain('Item B1');
|
|
2113
|
+
});
|
|
2114
|
+
});
|
|
@@ -18,6 +18,10 @@ const missingStylesMap = new Map<string, MissingStyleReport>();
|
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Check if a style property and value can be converted to a utility class
|
|
21
|
+
*
|
|
22
|
+
* All CSS properties now generate utility classes:
|
|
23
|
+
* - Known properties (in propertyMap) use standard prefixes
|
|
24
|
+
* - Unknown properties generate dynamic classes with auto-prefixes
|
|
21
25
|
*/
|
|
22
26
|
function canGenerateClass(property: string, value: unknown): boolean {
|
|
23
27
|
// Skip internal component properties that shouldn't be styles
|
|
@@ -30,11 +34,6 @@ function canGenerateClass(property: string, value: unknown): boolean {
|
|
|
30
34
|
return true; // Skip reporting these - they're not CSS styles
|
|
31
35
|
}
|
|
32
36
|
|
|
33
|
-
// Skip if property is not in our mapping
|
|
34
|
-
if (!propertyMap[property]) {
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
37
|
// Skip if value is null, undefined, or empty - these don't need utility classes
|
|
39
38
|
if (value === null || value === undefined || value === '') {
|
|
40
39
|
return true; // Skip reporting - nothing to convert
|
|
@@ -46,17 +45,15 @@ function canGenerateClass(property: string, value: unknown): boolean {
|
|
|
46
45
|
return false;
|
|
47
46
|
}
|
|
48
47
|
|
|
49
|
-
//
|
|
50
|
-
// But allow CSS var() functions since these can be converted to utility classes
|
|
48
|
+
// Object values cannot be converted to utility classes
|
|
51
49
|
if (typeof value === 'object') {
|
|
52
50
|
return false;
|
|
53
51
|
}
|
|
54
52
|
|
|
55
|
-
// All
|
|
56
|
-
// -
|
|
57
|
-
// -
|
|
58
|
-
// -
|
|
59
|
-
// - Everything else via fallback: any value → "prefix-value"
|
|
53
|
+
// All CSS properties (including unknown ones) now generate utility classes:
|
|
54
|
+
// - Known properties use standard prefixes from propertyMap
|
|
55
|
+
// - Unknown properties generate dynamic classes registered in the dynamic registry
|
|
56
|
+
// - CSS var() calls, space-separated values, and functions are all handled
|
|
60
57
|
return true;
|
|
61
58
|
}
|
|
62
59
|
|
|
@@ -115,10 +112,10 @@ export function printMissingStyleWarnings(verbose = false): void {
|
|
|
115
112
|
|
|
116
113
|
const warnings: string[] = [];
|
|
117
114
|
warnings.push(
|
|
118
|
-
'\n⚠️ WARNING: Found styles
|
|
115
|
+
'\n⚠️ WARNING: Found styles that cannot be converted to utility classes\n'
|
|
119
116
|
);
|
|
120
117
|
warnings.push(
|
|
121
|
-
'These styles use
|
|
118
|
+
'These styles use object values which cannot be serialized to class names:\n'
|
|
122
119
|
);
|
|
123
120
|
|
|
124
121
|
// Sort by frequency
|
|
@@ -144,9 +141,9 @@ export function printMissingStyleWarnings(verbose = false): void {
|
|
|
144
141
|
}
|
|
145
142
|
|
|
146
143
|
warnings.push('\n💡 Note:');
|
|
147
|
-
warnings.push(' •
|
|
148
|
-
warnings.push(' •
|
|
149
|
-
warnings.push(' •
|
|
144
|
+
warnings.push(' • All string/number values are now converted to utility classes');
|
|
145
|
+
warnings.push(' • Object values (non-primitive) cannot be converted and will remain as-is');
|
|
146
|
+
warnings.push(' • This warning only appears for object-type style values\n');
|
|
150
147
|
|
|
151
148
|
console.warn(warnings.join('\n'));
|
|
152
149
|
}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { describe, test, expect } from 'bun:test';
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
getAllBreakpointNames,
|
|
4
|
+
getBreakpointName,
|
|
5
|
+
DEFAULT_BREAKPOINTS,
|
|
6
|
+
normalizeBreakpointConfig,
|
|
7
|
+
getBreakpointValues,
|
|
8
|
+
getPreviewPointValues,
|
|
9
|
+
getBreakpointLabel,
|
|
10
|
+
} from './breakpoints';
|
|
11
|
+
import type { BreakpointConfig, BreakpointConfigInput } from './breakpoints';
|
|
4
12
|
|
|
5
13
|
/**
|
|
6
14
|
* breakpoints Tests
|
|
@@ -9,9 +17,118 @@ import type { BreakpointConfig } from './breakpoints';
|
|
|
9
17
|
|
|
10
18
|
describe('breakpoints', () => {
|
|
11
19
|
describe('DEFAULT_BREAKPOINTS', () => {
|
|
12
|
-
test('should have tablet and mobile breakpoints', () => {
|
|
13
|
-
expect(DEFAULT_BREAKPOINTS.tablet).toBe(1024);
|
|
14
|
-
expect(DEFAULT_BREAKPOINTS.
|
|
20
|
+
test('should have tablet and mobile breakpoints with new format', () => {
|
|
21
|
+
expect(DEFAULT_BREAKPOINTS.tablet.breakpoint).toBe(1024);
|
|
22
|
+
expect(DEFAULT_BREAKPOINTS.tablet.previewPoint).toBe(768);
|
|
23
|
+
expect(DEFAULT_BREAKPOINTS.mobile.breakpoint).toBe(540);
|
|
24
|
+
expect(DEFAULT_BREAKPOINTS.mobile.previewPoint).toBe(375);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('normalizeBreakpointConfig', () => {
|
|
29
|
+
test('should convert legacy number format to object format', () => {
|
|
30
|
+
const input: BreakpointConfigInput = {
|
|
31
|
+
tablet: 1024,
|
|
32
|
+
mobile: 540,
|
|
33
|
+
};
|
|
34
|
+
const result = normalizeBreakpointConfig(input);
|
|
35
|
+
|
|
36
|
+
expect(result.tablet).toEqual({ breakpoint: 1024, previewPoint: 1024 });
|
|
37
|
+
expect(result.mobile).toEqual({ breakpoint: 540, previewPoint: 540 });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('should preserve new object format', () => {
|
|
41
|
+
const input: BreakpointConfigInput = {
|
|
42
|
+
tablet: { breakpoint: 1024, previewPoint: 768 },
|
|
43
|
+
mobile: { breakpoint: 540, previewPoint: 375 },
|
|
44
|
+
};
|
|
45
|
+
const result = normalizeBreakpointConfig(input);
|
|
46
|
+
|
|
47
|
+
expect(result.tablet).toEqual({ breakpoint: 1024, previewPoint: 768 });
|
|
48
|
+
expect(result.mobile).toEqual({ breakpoint: 540, previewPoint: 375 });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('should handle mixed format', () => {
|
|
52
|
+
const input: BreakpointConfigInput = {
|
|
53
|
+
tablet: { breakpoint: 1024, previewPoint: 768 },
|
|
54
|
+
mobile: 540, // legacy
|
|
55
|
+
};
|
|
56
|
+
const result = normalizeBreakpointConfig(input);
|
|
57
|
+
|
|
58
|
+
expect(result.tablet).toEqual({ breakpoint: 1024, previewPoint: 768 });
|
|
59
|
+
expect(result.mobile).toEqual({ breakpoint: 540, previewPoint: 540 });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('should default previewPoint to breakpoint if not specified', () => {
|
|
63
|
+
const input: BreakpointConfigInput = {
|
|
64
|
+
tablet: { breakpoint: 1024 } as any, // Missing previewPoint
|
|
65
|
+
};
|
|
66
|
+
const result = normalizeBreakpointConfig(input);
|
|
67
|
+
|
|
68
|
+
expect(result.tablet).toEqual({ breakpoint: 1024, previewPoint: 1024 });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('should handle empty input', () => {
|
|
72
|
+
const result = normalizeBreakpointConfig({});
|
|
73
|
+
expect(result).toEqual({});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('should preserve label when present', () => {
|
|
77
|
+
const input: BreakpointConfigInput = {
|
|
78
|
+
tablet: { breakpoint: 1024, previewPoint: 768, label: 'Tablet Landscape' },
|
|
79
|
+
mobile: { breakpoint: 540, previewPoint: 375, label: 'Phone' },
|
|
80
|
+
};
|
|
81
|
+
const result = normalizeBreakpointConfig(input);
|
|
82
|
+
|
|
83
|
+
expect(result.tablet).toEqual({ breakpoint: 1024, previewPoint: 768, label: 'Tablet Landscape' });
|
|
84
|
+
expect(result.mobile).toEqual({ breakpoint: 540, previewPoint: 375, label: 'Phone' });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('should not include label when not present', () => {
|
|
88
|
+
const input: BreakpointConfigInput = {
|
|
89
|
+
tablet: { breakpoint: 1024, previewPoint: 768 },
|
|
90
|
+
};
|
|
91
|
+
const result = normalizeBreakpointConfig(input);
|
|
92
|
+
|
|
93
|
+
expect(result.tablet).toEqual({ breakpoint: 1024, previewPoint: 768 });
|
|
94
|
+
expect(result.tablet).not.toHaveProperty('label');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('should handle mixed entries with and without labels', () => {
|
|
98
|
+
const input: BreakpointConfigInput = {
|
|
99
|
+
tabletLandscape: { breakpoint: 1024, previewPoint: 900, label: 'Tablet Landscape' },
|
|
100
|
+
tabletPortrait: { breakpoint: 768, previewPoint: 700 }, // no label
|
|
101
|
+
mobile: 540, // legacy format
|
|
102
|
+
};
|
|
103
|
+
const result = normalizeBreakpointConfig(input);
|
|
104
|
+
|
|
105
|
+
expect(result.tabletLandscape.label).toBe('Tablet Landscape');
|
|
106
|
+
expect(result.tabletPortrait).not.toHaveProperty('label');
|
|
107
|
+
expect(result.mobile).not.toHaveProperty('label');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('getBreakpointValues', () => {
|
|
112
|
+
test('should extract breakpoint values', () => {
|
|
113
|
+
const config: BreakpointConfig = {
|
|
114
|
+
tablet: { breakpoint: 1024, previewPoint: 768 },
|
|
115
|
+
mobile: { breakpoint: 540, previewPoint: 375 },
|
|
116
|
+
};
|
|
117
|
+
const result = getBreakpointValues(config);
|
|
118
|
+
|
|
119
|
+
expect(result).toEqual({ tablet: 1024, mobile: 540 });
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('getPreviewPointValues', () => {
|
|
124
|
+
test('should extract preview point values', () => {
|
|
125
|
+
const config: BreakpointConfig = {
|
|
126
|
+
tablet: { breakpoint: 1024, previewPoint: 768 },
|
|
127
|
+
mobile: { breakpoint: 540, previewPoint: 375 },
|
|
128
|
+
};
|
|
129
|
+
const result = getPreviewPointValues(config);
|
|
130
|
+
|
|
131
|
+
expect(result).toEqual({ tablet: 768, mobile: 375 });
|
|
15
132
|
});
|
|
16
133
|
});
|
|
17
134
|
|
|
@@ -39,9 +156,9 @@ describe('breakpoints', () => {
|
|
|
39
156
|
|
|
40
157
|
test('should handle custom breakpoints', () => {
|
|
41
158
|
const customBreakpoints: BreakpointConfig = {
|
|
42
|
-
large: 1440,
|
|
43
|
-
medium: 960,
|
|
44
|
-
small: 480,
|
|
159
|
+
large: { breakpoint: 1440, previewPoint: 1200 },
|
|
160
|
+
medium: { breakpoint: 960, previewPoint: 800 },
|
|
161
|
+
small: { breakpoint: 480, previewPoint: 375 },
|
|
45
162
|
};
|
|
46
163
|
const names = getAllBreakpointNames(customBreakpoints);
|
|
47
164
|
expect(names).toEqual(['base', 'large', 'medium', 'small']);
|
|
@@ -49,11 +166,11 @@ describe('breakpoints', () => {
|
|
|
49
166
|
|
|
50
167
|
test('should sort custom breakpoints correctly', () => {
|
|
51
168
|
const customBreakpoints: BreakpointConfig = {
|
|
52
|
-
xs: 320,
|
|
53
|
-
xl: 1920,
|
|
54
|
-
md: 768,
|
|
55
|
-
sm: 480,
|
|
56
|
-
lg: 1280,
|
|
169
|
+
xs: { breakpoint: 320, previewPoint: 320 },
|
|
170
|
+
xl: { breakpoint: 1920, previewPoint: 1920 },
|
|
171
|
+
md: { breakpoint: 768, previewPoint: 768 },
|
|
172
|
+
sm: { breakpoint: 480, previewPoint: 480 },
|
|
173
|
+
lg: { breakpoint: 1280, previewPoint: 1280 },
|
|
57
174
|
};
|
|
58
175
|
const names = getAllBreakpointNames(customBreakpoints);
|
|
59
176
|
expect(names).toEqual(['base', 'xl', 'lg', 'md', 'sm', 'xs']);
|
|
@@ -65,15 +182,15 @@ describe('breakpoints', () => {
|
|
|
65
182
|
});
|
|
66
183
|
|
|
67
184
|
test('should handle single breakpoint', () => {
|
|
68
|
-
const names = getAllBreakpointNames({ mobile: 640 });
|
|
185
|
+
const names = getAllBreakpointNames({ mobile: { breakpoint: 640, previewPoint: 375 } });
|
|
69
186
|
expect(names).toEqual(['base', 'mobile']);
|
|
70
187
|
});
|
|
71
188
|
|
|
72
189
|
test('should handle breakpoints with same values', () => {
|
|
73
190
|
const customBreakpoints: BreakpointConfig = {
|
|
74
|
-
a: 1024,
|
|
75
|
-
b: 1024,
|
|
76
|
-
c: 540,
|
|
191
|
+
a: { breakpoint: 1024, previewPoint: 1024 },
|
|
192
|
+
b: { breakpoint: 1024, previewPoint: 768 },
|
|
193
|
+
c: { breakpoint: 540, previewPoint: 375 },
|
|
77
194
|
};
|
|
78
195
|
const names = getAllBreakpointNames(customBreakpoints);
|
|
79
196
|
expect(names).toContain('base');
|
|
@@ -115,9 +232,9 @@ describe('breakpoints', () => {
|
|
|
115
232
|
|
|
116
233
|
test('should work with custom breakpoints', () => {
|
|
117
234
|
const customBreakpoints: BreakpointConfig = {
|
|
118
|
-
large: 1440,
|
|
119
|
-
medium: 960,
|
|
120
|
-
small: 480,
|
|
235
|
+
large: { breakpoint: 1440, previewPoint: 1200 },
|
|
236
|
+
medium: { breakpoint: 960, previewPoint: 800 },
|
|
237
|
+
small: { breakpoint: 480, previewPoint: 375 },
|
|
121
238
|
};
|
|
122
239
|
|
|
123
240
|
expect(getBreakpointName(1920, customBreakpoints)).toBe('base');
|
|
@@ -150,9 +267,9 @@ describe('breakpoints', () => {
|
|
|
150
267
|
|
|
151
268
|
test('should find correct breakpoint when multiple are similar', () => {
|
|
152
269
|
const customBreakpoints: BreakpointConfig = {
|
|
153
|
-
a: 1000,
|
|
154
|
-
b: 999,
|
|
155
|
-
c: 500,
|
|
270
|
+
a: { breakpoint: 1000, previewPoint: 1000 },
|
|
271
|
+
b: { breakpoint: 999, previewPoint: 999 },
|
|
272
|
+
c: { breakpoint: 500, previewPoint: 500 },
|
|
156
273
|
};
|
|
157
274
|
|
|
158
275
|
expect(getBreakpointName(1001, customBreakpoints)).toBe('base');
|
|
@@ -162,5 +279,75 @@ describe('breakpoints', () => {
|
|
|
162
279
|
expect(getBreakpointName(500, customBreakpoints)).toBe('c');
|
|
163
280
|
expect(getBreakpointName(499, customBreakpoints)).toBe('c');
|
|
164
281
|
});
|
|
282
|
+
|
|
283
|
+
test('should use breakpoint value not previewPoint for determination', () => {
|
|
284
|
+
const customBreakpoints: BreakpointConfig = {
|
|
285
|
+
tablet: { breakpoint: 1024, previewPoint: 768 },
|
|
286
|
+
mobile: { breakpoint: 540, previewPoint: 375 },
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// Should use breakpoint (1024) not previewPoint (768) for tablet threshold
|
|
290
|
+
expect(getBreakpointName(1024, customBreakpoints)).toBe('tablet');
|
|
291
|
+
expect(getBreakpointName(768, customBreakpoints)).toBe('tablet');
|
|
292
|
+
expect(getBreakpointName(1025, customBreakpoints)).toBe('base');
|
|
293
|
+
|
|
294
|
+
// Should use breakpoint (540) not previewPoint (375) for mobile threshold
|
|
295
|
+
expect(getBreakpointName(540, customBreakpoints)).toBe('mobile');
|
|
296
|
+
expect(getBreakpointName(375, customBreakpoints)).toBe('mobile');
|
|
297
|
+
expect(getBreakpointName(541, customBreakpoints)).toBe('tablet');
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe('getBreakpointLabel', () => {
|
|
302
|
+
test('should return "Desktop" for base', () => {
|
|
303
|
+
expect(getBreakpointLabel('base')).toBe('Desktop');
|
|
304
|
+
expect(getBreakpointLabel('base', DEFAULT_BREAKPOINTS)).toBe('Desktop');
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test('should return custom label when set in config', () => {
|
|
308
|
+
const customBreakpoints: BreakpointConfig = {
|
|
309
|
+
tablet: { breakpoint: 1024, previewPoint: 768, label: 'Tablet Landscape' },
|
|
310
|
+
mobile: { breakpoint: 540, previewPoint: 375, label: 'Phone' },
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
expect(getBreakpointLabel('tablet', customBreakpoints)).toBe('Tablet Landscape');
|
|
314
|
+
expect(getBreakpointLabel('mobile', customBreakpoints)).toBe('Phone');
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test('should auto-capitalize simple names when no label set', () => {
|
|
318
|
+
expect(getBreakpointLabel('tablet')).toBe('Tablet');
|
|
319
|
+
expect(getBreakpointLabel('mobile')).toBe('Mobile');
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test('should convert camelCase to Title Case when no label set', () => {
|
|
323
|
+
const customBreakpoints: BreakpointConfig = {
|
|
324
|
+
tabletLandscape: { breakpoint: 1024, previewPoint: 900 },
|
|
325
|
+
tabletPortrait: { breakpoint: 768, previewPoint: 700 },
|
|
326
|
+
smallPhone: { breakpoint: 320, previewPoint: 320 },
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
expect(getBreakpointLabel('tabletLandscape', customBreakpoints)).toBe('Tablet Landscape');
|
|
330
|
+
expect(getBreakpointLabel('tabletPortrait', customBreakpoints)).toBe('Tablet Portrait');
|
|
331
|
+
expect(getBreakpointLabel('smallPhone', customBreakpoints)).toBe('Small Phone');
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test('should handle breakpoint not in config', () => {
|
|
335
|
+
// If breakpoint doesn't exist in config, should still format the name
|
|
336
|
+
expect(getBreakpointLabel('unknownBreakpoint', DEFAULT_BREAKPOINTS)).toBe('Unknown Breakpoint');
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test('should prefer label over auto-formatting', () => {
|
|
340
|
+
const customBreakpoints: BreakpointConfig = {
|
|
341
|
+
tabletLandscape: { breakpoint: 1024, previewPoint: 900, label: 'iPad Pro' },
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
// Should use the label, not auto-format "tabletLandscape"
|
|
345
|
+
expect(getBreakpointLabel('tabletLandscape', customBreakpoints)).toBe('iPad Pro');
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test('should handle empty breakpoints config', () => {
|
|
349
|
+
expect(getBreakpointLabel('tablet', {})).toBe('Tablet');
|
|
350
|
+
expect(getBreakpointLabel('customBreakpoint', {})).toBe('Custom Breakpoint');
|
|
351
|
+
});
|
|
165
352
|
});
|
|
166
353
|
});
|