From 8d0696584c2b39bf781e094d82d2992707187b35 Mon Sep 17 00:00:00 2001 From: Serge RAKOTO HARRY-NAIVO Date: Mon, 27 Apr 2026 19:52:27 +0200 Subject: [PATCH] feat(config): parse data-shadow attribute on script tag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/config.ts | 4 +- tests/unit/config.test.ts | 118 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 tests/unit/config.test.ts diff --git a/src/config.ts b/src/config.ts index 05ab8e0..40fa54c 100644 --- a/src/config.ts +++ b/src/config.ts @@ -3,6 +3,7 @@ export type WidgetConfig = { apiKey: string; serverUrl: string; position?: 'bottom-right' | 'bottom-left'; + shadow: boolean; }; export function parseConfig(): WidgetConfig | null { @@ -22,10 +23,11 @@ export function parseConfig(): WidgetConfig | null { | 'bottom-right' | 'bottom-left' | null) ?? 'bottom-right'; + const shadow = script.getAttribute('data-shadow') === 'true'; if (!botId || !apiKey) return null; - return { botId, apiKey, serverUrl, position }; + return { botId, apiKey, serverUrl, position, shadow }; } function deriveServerFromSrc(src: string): string { diff --git a/tests/unit/config.test.ts b/tests/unit/config.test.ts new file mode 100644 index 0000000..a107ce3 --- /dev/null +++ b/tests/unit/config.test.ts @@ -0,0 +1,118 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { parseConfig } from '@/config'; + +function createScript( + attrs: Record, + 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); + }); + }); +});