feat(config): parse data-shadow attribute on script tag

Adds the boolean field consumed by the upcoming Shadow DOM mount path.
Strict equality with the string "true" — anything else (absent, "false",
"1", "yes", empty) yields false, so accidental opt-in is impossible.

Tests cover the full parseConfig surface (required fields, server-url
derivation, position default) plus the new shadow attribute parsing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serge RAKOTO HARRY-NAIVO 2026-04-27 19:52:27 +02:00
parent ca084735ac
commit 8d0696584c
2 changed files with 121 additions and 1 deletions

View File

@ -3,6 +3,7 @@ export type WidgetConfig = {
apiKey: string; apiKey: string;
serverUrl: string; serverUrl: string;
position?: 'bottom-right' | 'bottom-left'; position?: 'bottom-right' | 'bottom-left';
shadow: boolean;
}; };
export function parseConfig(): WidgetConfig | null { export function parseConfig(): WidgetConfig | null {
@ -22,10 +23,11 @@ export function parseConfig(): WidgetConfig | null {
| 'bottom-right' | 'bottom-right'
| 'bottom-left' | 'bottom-left'
| null) ?? 'bottom-right'; | null) ?? 'bottom-right';
const shadow = script.getAttribute('data-shadow') === 'true';
if (!botId || !apiKey) return null; if (!botId || !apiKey) return null;
return { botId, apiKey, serverUrl, position }; return { botId, apiKey, serverUrl, position, shadow };
} }
function deriveServerFromSrc(src: string): string { function deriveServerFromSrc(src: string): string {

118
tests/unit/config.test.ts Normal file
View File

@ -0,0 +1,118 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { parseConfig } from '@/config';
function createScript(
attrs: Record<string, string>,
src = 'https://cdn.example.com/messenzy-widget.iife.js',
): HTMLScriptElement {
const script = document.createElement('script');
script.src = src;
for (const [key, value] of Object.entries(attrs)) {
script.setAttribute(key, value);
}
document.head.appendChild(script);
return script;
}
beforeEach(() => {
document.head.innerHTML = '';
document.body.innerHTML = '';
});
describe('parseConfig', () => {
it('returns null when no script with data-bot-id is present', () => {
expect(parseConfig()).toBeNull();
});
it('returns null when data-api-key is missing', () => {
createScript({ 'data-bot-id': 'b1' });
expect(parseConfig()).toBeNull();
});
it('returns null when data-bot-id is missing', () => {
createScript({ 'data-api-key': 'k1' });
expect(parseConfig()).toBeNull();
});
it('parses required fields', () => {
createScript({ 'data-bot-id': 'b1', 'data-api-key': 'k1' });
const config = parseConfig();
expect(config).not.toBeNull();
expect(config?.botId).toBe('b1');
expect(config?.apiKey).toBe('k1');
});
it('derives serverUrl from script src when data-server-url absent', () => {
createScript(
{ 'data-bot-id': 'b1', 'data-api-key': 'k1' },
'https://widgets.example.com/path/messenzy.iife.js',
);
expect(parseConfig()?.serverUrl).toBe('https://widgets.example.com');
});
it('respects explicit data-server-url override', () => {
createScript({
'data-bot-id': 'b1',
'data-api-key': 'k1',
'data-server-url': 'https://api.foo.test',
});
expect(parseConfig()?.serverUrl).toBe('https://api.foo.test');
});
it('defaults position to bottom-right', () => {
createScript({ 'data-bot-id': 'b1', 'data-api-key': 'k1' });
expect(parseConfig()?.position).toBe('bottom-right');
});
it('parses position bottom-left', () => {
createScript({
'data-bot-id': 'b1',
'data-api-key': 'k1',
'data-position': 'bottom-left',
});
expect(parseConfig()?.position).toBe('bottom-left');
});
describe('shadow attribute (PR B opt-in)', () => {
it('shadow=false when data-shadow absent', () => {
createScript({ 'data-bot-id': 'b1', 'data-api-key': 'k1' });
expect(parseConfig()?.shadow).toBe(false);
});
it('shadow=true when data-shadow="true"', () => {
createScript({
'data-bot-id': 'b1',
'data-api-key': 'k1',
'data-shadow': 'true',
});
expect(parseConfig()?.shadow).toBe(true);
});
it('shadow=false when data-shadow="false"', () => {
createScript({
'data-bot-id': 'b1',
'data-api-key': 'k1',
'data-shadow': 'false',
});
expect(parseConfig()?.shadow).toBe(false);
});
it('shadow=false when data-shadow="" (empty)', () => {
createScript({
'data-bot-id': 'b1',
'data-api-key': 'k1',
'data-shadow': '',
});
expect(parseConfig()?.shadow).toBe(false);
});
it('shadow=false for unrecognized values (e.g. "1", "yes")', () => {
createScript({
'data-bot-id': 'b1',
'data-api-key': 'k1',
'data-shadow': '1',
});
expect(parseConfig()?.shadow).toBe(false);
});
});
});