Skip to content

Commit b0a1b0e

Browse files
authored
Merge pull request #198 from jasonbrd/dev
White Label Support
2 parents db8ff04 + 70c581b commit b0a1b0e

38 files changed

+3955
-39
lines changed

.dockerignore

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,36 @@
22
.env.local
33
node_modules
44
test-results
5+
6+
# Test files and directories
7+
__tests__
8+
tests
9+
*.test.ts
10+
*.test.tsx
11+
*.test.js
12+
*.test.jsx
13+
*.spec.ts
14+
*.spec.tsx
15+
*.spec.js
16+
*.spec.jsx
17+
vitest.config.ts
18+
.vitest
19+
20+
# Development and IDE files
21+
.kiro
22+
.vscode
23+
.idea
24+
*.log
25+
npm-debug.log*
26+
yarn-debug.log*
27+
28+
# Documentation (optional - remove if you want docs in container)
29+
# docs
30+
31+
# Git files
32+
.git
33+
.gitignore
34+
.gitattributes
35+
36+
# CI/CD files
37+
.github

.env.local.whitelabel.example

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# White Label Configuration Example
2+
# Copy this file to .env.local and customize the values for your organization
3+
4+
# ============================================================================
5+
# CUSTOM LOGO CONFIGURATION
6+
# ============================================================================
7+
# Path to your custom logo file (relative to public/logos/ directory)
8+
# The logo should be an SVG file for best quality at all sizes
9+
# If not specified, the default application logo will be used
10+
# Example: NEXT_PUBLIC_CUSTOM_LOGO=my-company-logo.svg
11+
NEXT_PUBLIC_CUSTOM_LOGO=
12+
13+
# ============================================================================
14+
# DEFAULT THEME CONFIGURATION
15+
# ============================================================================
16+
# Set the default theme that users see when they first visit the application
17+
# Valid values: "light" or "dark"
18+
# If not specified, defaults to "light"
19+
# Note: Users can override this with their own preference, which will be saved
20+
# Example: NEXT_PUBLIC_DEFAULT_THEME=dark
21+
NEXT_PUBLIC_DEFAULT_THEME=light
22+
23+
# ============================================================================
24+
# BRAND NAME CONFIGURATION
25+
# ============================================================================
26+
# Your organization's brand name
27+
# This will be used in the logo alt text and other branding contexts
28+
# If not specified, defaults to "Amplify GenAI"
29+
# Example: NEXT_PUBLIC_BRAND_NAME=Acme Corporation
30+
NEXT_PUBLIC_BRAND_NAME=
31+
32+
# ============================================================================
33+
# USAGE INSTRUCTIONS
34+
# ============================================================================
35+
# 1. Copy this file to .env.local in your project root
36+
# 2. Uncomment and set the values you want to customize
37+
# 3. Place your custom logo SVG file in the public/logos/ directory
38+
# 4. Rebuild your application for changes to take effect
39+
# 5. See docs/WHITE_LABEL.md for detailed customization instructions
40+
#
41+
# For color customization, edit tailwind.config.js - see WHITE_LABEL.md

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,7 @@ __pycache__/
5252
*.pyc
5353
*.pyo
5454
.pytest_cache/
55-
tests/chrome_profile/
55+
tests/chrome_profile/
56+
57+
# Kiro IDE specific files
58+
.kiro/

.vscode/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
{
2+
}

Dockerfile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@
1717

1818
# ---- Build ----
1919
FROM dependencies AS build
20+
21+
# Accept white label build arguments
22+
ARG NEXT_PUBLIC_CUSTOM_LOGO
23+
ARG NEXT_PUBLIC_DEFAULT_THEME
24+
ARG NEXT_PUBLIC_BRAND_NAME
25+
26+
# Set as environment variables for Next.js build
27+
ENV NEXT_PUBLIC_CUSTOM_LOGO=${NEXT_PUBLIC_CUSTOM_LOGO}
28+
ENV NEXT_PUBLIC_DEFAULT_THEME=${NEXT_PUBLIC_DEFAULT_THEME}
29+
ENV NEXT_PUBLIC_BRAND_NAME=${NEXT_PUBLIC_BRAND_NAME}
30+
2031
COPY --chown=appuser:appgroup . .
2132
RUN npm run build
2233

__tests__/components/Logo.test.tsx

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
/**
2+
* Property-Based Tests for Logo Component
3+
*
4+
* Tests the Logo component logic using property-based testing with fast-check.
5+
* Each test runs a minimum of 100 iterations to verify properties hold across all inputs.
6+
*
7+
* Note: These tests verify the logo selection logic by testing the configuration
8+
* and path resolution behavior that the Logo component relies on.
9+
*/
10+
11+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
12+
import { fc } from '@fast-check/vitest';
13+
import { getWhiteLabelConfig } from '@/utils/whiteLabel/config';
14+
15+
describe('Logo Component - Property Tests', () => {
16+
let consoleWarnSpy: ReturnType<typeof vi.spyOn>;
17+
18+
beforeEach(() => {
19+
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
20+
});
21+
22+
afterEach(() => {
23+
delete process.env.NEXT_PUBLIC_CUSTOM_LOGO;
24+
delete process.env.NEXT_PUBLIC_DEFAULT_THEME;
25+
delete process.env.NEXT_PUBLIC_BRAND_NAME;
26+
consoleWarnSpy.mockRestore();
27+
});
28+
29+
/**
30+
* Helper function to determine logo source based on configuration
31+
* This mirrors the logic in the Logo component
32+
*/
33+
const getLogoSrc = (customLogoPath: string | null): string => {
34+
if (!customLogoPath) {
35+
return '/sparc_apple.png';
36+
}
37+
38+
if (!customLogoPath.toLowerCase().endsWith('.svg')) {
39+
console.warn(
40+
`Custom logo "${customLogoPath}" does not have .svg extension, falling back to default`
41+
);
42+
return '/sparc_apple.png';
43+
}
44+
45+
return `/logos/${customLogoPath}`;
46+
};
47+
48+
/**
49+
* Feature: white-labeling, Property 1: Default logo fallback
50+
* Validates: Requirements 1.1
51+
*
52+
* For any application start state, when no custom logo path is configured,
53+
* the system should display the default logo.
54+
*/
55+
it('Property 1: Default logo fallback - should use default logo when no custom logo configured', () => {
56+
fc.assert(
57+
fc.property(
58+
// Generate arbitrary brand names and themes
59+
fc.string({ minLength: 1, maxLength: 50 }),
60+
fc.constantFrom('light' as const, 'dark' as const),
61+
(brandName, theme) => {
62+
// Set environment without custom logo
63+
delete process.env.NEXT_PUBLIC_CUSTOM_LOGO;
64+
process.env.NEXT_PUBLIC_BRAND_NAME = brandName;
65+
process.env.NEXT_PUBLIC_DEFAULT_THEME = theme;
66+
67+
const config = getWhiteLabelConfig();
68+
const logoSrc = getLogoSrc(config.customLogoPath);
69+
70+
// Verify default logo is used
71+
expect(config.customLogoPath).toBeNull();
72+
expect(logoSrc).toBe('/sparc_apple.png');
73+
}
74+
),
75+
{ numRuns: 100 }
76+
);
77+
});
78+
79+
/**
80+
* Feature: white-labeling, Property 2: Custom logo loading
81+
* Validates: Requirements 1.2
82+
*
83+
* For any valid SVG filename in the public/logos directory, when that filename
84+
* is set as the custom logo environment variable, the system should load that
85+
* specific file as the logo source.
86+
*/
87+
it('Property 2: Custom logo loading - should load custom SVG logo from logos directory', () => {
88+
fc.assert(
89+
fc.property(
90+
// Generate valid SVG filenames
91+
fc.string({ minLength: 1, maxLength: 30 })
92+
.filter(s => !s.includes('/') && !s.includes('\\') && s.trim().length > 0)
93+
.map(s => `${s.replace(/\s+/g, '-')}.svg`),
94+
(svgFilename) => {
95+
// Set custom logo environment variable
96+
process.env.NEXT_PUBLIC_CUSTOM_LOGO = svgFilename;
97+
98+
const config = getWhiteLabelConfig();
99+
const logoSrc = getLogoSrc(config.customLogoPath);
100+
101+
// Verify custom logo path is set and correct source is generated
102+
expect(config.customLogoPath).toBe(svgFilename);
103+
expect(logoSrc).toBe(`/logos/${svgFilename}`);
104+
}
105+
),
106+
{ numRuns: 100 }
107+
);
108+
});
109+
110+
/**
111+
* Feature: white-labeling, Property 3: Logo error handling
112+
* Validates: Requirements 1.3
113+
*
114+
* For any non-existent or invalid logo file path, the system should fall back
115+
* to the default logo and log a warning message.
116+
*
117+
* Note: This test verifies the fallback logic. The actual file existence check
118+
* happens at runtime in the browser when the Image component attempts to load.
119+
* The Logo component's onError handler triggers the fallback behavior.
120+
*/
121+
it('Property 3: Logo error handling - should handle error state and fall back to default', () => {
122+
// Test that when hasError state is true, default logo is used
123+
// This simulates what happens after onError is triggered
124+
const getLogoSrcWithError = (customLogoPath: string | null, hasError: boolean): string => {
125+
if (hasError || !customLogoPath) {
126+
return '/sparc_apple.png';
127+
}
128+
129+
if (!customLogoPath.toLowerCase().endsWith('.svg')) {
130+
return '/sparc_apple.png';
131+
}
132+
133+
return `/logos/${customLogoPath}`;
134+
};
135+
136+
fc.assert(
137+
fc.property(
138+
// Generate arbitrary logo paths
139+
fc.string({ minLength: 1, maxLength: 50 }),
140+
(logoPath) => {
141+
// Simulate error state (as would happen after onError callback)
142+
const logoSrcAfterError = getLogoSrcWithError(logoPath, true);
143+
144+
// Verify fallback to default logo
145+
expect(logoSrcAfterError).toBe('/sparc_apple.png');
146+
}
147+
),
148+
{ numRuns: 100 }
149+
);
150+
});
151+
152+
/**
153+
* Feature: white-labeling, Property 4: Logo extension validation
154+
* Validates: Requirements 1.4
155+
*
156+
* For any configured logo path, if the file extension is not '.svg',
157+
* the system should handle it appropriately (reject or fall back).
158+
*/
159+
it('Property 4: Logo extension validation - should fall back to default for non-SVG extensions', () => {
160+
fc.assert(
161+
fc.property(
162+
// Generate filenames with non-SVG extensions
163+
fc.string({ minLength: 1, maxLength: 20 })
164+
.filter(s => s.trim().length > 0 && !s.includes('/') && !s.includes('\\')),
165+
fc.constantFrom('.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.ico', '.txt', ''),
166+
(filename, extension) => {
167+
consoleWarnSpy.mockClear();
168+
169+
const logoPath = `${filename}${extension}`;
170+
process.env.NEXT_PUBLIC_CUSTOM_LOGO = logoPath;
171+
172+
const config = getWhiteLabelConfig();
173+
const logoSrc = getLogoSrc(config.customLogoPath);
174+
175+
// If extension is not .svg, should fall back to default
176+
if (!extension.toLowerCase().endsWith('.svg')) {
177+
expect(logoSrc).toBe('/sparc_apple.png');
178+
179+
// Should log warning for non-empty extensions
180+
if (extension.length > 0) {
181+
expect(consoleWarnSpy).toHaveBeenCalled();
182+
}
183+
}
184+
}
185+
),
186+
{ numRuns: 100 }
187+
);
188+
});
189+
190+
/**
191+
* Feature: white-labeling, Property 4: Logo extension validation (positive case)
192+
* Validates: Requirements 1.4
193+
*
194+
* For any configured logo path with .svg extension, the system should accept it.
195+
*/
196+
it('Property 4: Logo extension validation - should accept SVG extensions (case insensitive)', () => {
197+
fc.assert(
198+
fc.property(
199+
// Generate filenames with SVG extensions in various cases
200+
fc.string({ minLength: 1, maxLength: 20 })
201+
.filter(s => s.trim().length > 0 && !s.includes('/') && !s.includes('\\')),
202+
fc.constantFrom('.svg', '.SVG', '.Svg', '.sVg'),
203+
(filename, extension) => {
204+
consoleWarnSpy.mockClear();
205+
206+
const logoPath = `${filename}${extension}`;
207+
process.env.NEXT_PUBLIC_CUSTOM_LOGO = logoPath;
208+
209+
const config = getWhiteLabelConfig();
210+
const logoSrc = getLogoSrc(config.customLogoPath);
211+
212+
// Should accept SVG extension (case insensitive)
213+
expect(logoSrc).toBe(`/logos/${logoPath}`);
214+
expect(consoleWarnSpy).not.toHaveBeenCalled();
215+
}
216+
),
217+
{ numRuns: 100 }
218+
);
219+
});
220+
221+
/**
222+
* Feature: white-labeling, Property 5: Logo consistency
223+
* Validates: Requirements 1.5
224+
*
225+
* For any page in the application, the logo component should render with
226+
* consistent dimensions and styling properties.
227+
*
228+
* Note: This test verifies that the logo source determination is consistent
229+
* across multiple calls with the same configuration.
230+
*/
231+
it('Property 5: Logo consistency - should return consistent logo source for same configuration', () => {
232+
fc.assert(
233+
fc.property(
234+
// Generate arbitrary logo configurations
235+
fc.option(
236+
fc.string({ minLength: 1, maxLength: 30 })
237+
.filter(s => !s.includes('/') && !s.includes('\\') && s.trim().length > 0)
238+
.map(s => `${s.replace(/\s+/g, '-')}.svg`),
239+
{ nil: null }
240+
),
241+
fc.string({ minLength: 1, maxLength: 50 }),
242+
(customLogo, brandName) => {
243+
// Set configuration
244+
if (customLogo) {
245+
process.env.NEXT_PUBLIC_CUSTOM_LOGO = customLogo;
246+
} else {
247+
delete process.env.NEXT_PUBLIC_CUSTOM_LOGO;
248+
}
249+
process.env.NEXT_PUBLIC_BRAND_NAME = brandName;
250+
251+
// Get logo source multiple times
252+
const config1 = getWhiteLabelConfig();
253+
const logoSrc1 = getLogoSrc(config1.customLogoPath);
254+
255+
const config2 = getWhiteLabelConfig();
256+
const logoSrc2 = getLogoSrc(config2.customLogoPath);
257+
258+
const config3 = getWhiteLabelConfig();
259+
const logoSrc3 = getLogoSrc(config3.customLogoPath);
260+
261+
// Verify consistency across multiple calls
262+
expect(logoSrc1).toBe(logoSrc2);
263+
expect(logoSrc2).toBe(logoSrc3);
264+
expect(config1.customLogoPath).toBe(config2.customLogoPath);
265+
expect(config2.customLogoPath).toBe(config3.customLogoPath);
266+
expect(config1.brandName).toBe(config2.brandName);
267+
expect(config2.brandName).toBe(config3.brandName);
268+
}
269+
),
270+
{ numRuns: 100 }
271+
);
272+
});
273+
});

0 commit comments

Comments
 (0)