diff --git a/dashboards/admin-web/src/__tests__/product-context.test.tsx b/dashboards/admin-web/src/__tests__/product-context.test.tsx
new file mode 100644
index 00000000..82d3bfa5
--- /dev/null
+++ b/dashboards/admin-web/src/__tests__/product-context.test.tsx
@@ -0,0 +1,82 @@
+// @vitest-environment happy-dom
+
+import { afterEach, beforeEach, describe, expect, it } from 'vitest';
+import { act } from 'react';
+import { createRoot, type Root } from 'react-dom/client';
+
+import { ProductProvider, useProduct } from '@/lib/product-context';
+
+function ProductProbe() {
+ const { productId, productName, setProductId } = useProduct();
+ return (
+
+
+
+
+
+ );
+}
+
+describe('ProductProvider', () => {
+ let container: HTMLDivElement;
+ let root: Root;
+
+ beforeEach(() => {
+ (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT =
+ true;
+ localStorage.clear();
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ root = createRoot(container);
+ });
+
+ afterEach(() => {
+ act(() => root.unmount());
+ container.remove();
+ localStorage.clear();
+ });
+
+ it('persists changes and emits a same-tab product-changed event', () => {
+ let eventCount = 0;
+ window.addEventListener('admin:product-changed', () => {
+ eventCount += 1;
+ });
+
+ act(() => {
+ root.render(
+
+
+
+ );
+ });
+
+ act(() => {
+ container.querySelector('button')!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+ });
+
+ expect(container.querySelector('[data-testid="product-id"]')?.textContent).toBe('nomgap');
+ expect(container.querySelector('[data-testid="product-name"]')?.textContent).toBe('NomGap');
+ expect(localStorage.getItem('admin_selected_product')).toBe('nomgap');
+ expect(eventCount).toBe(1);
+ });
+
+ it('syncs provider state when another admin component updates localStorage', () => {
+ act(() => {
+ root.render(
+
+
+
+ );
+ });
+
+ act(() => {
+ localStorage.setItem('admin_selected_product', 'mindlyst');
+ window.dispatchEvent(new Event('admin:product-changed'));
+ });
+
+ expect(container.querySelector('[data-testid="product-id"]')?.textContent).toBe('mindlyst');
+ expect(container.querySelector('[data-testid="product-name"]')?.textContent).toBe('MindLyst');
+ });
+});
diff --git a/dashboards/admin-web/src/lib/product-context.tsx b/dashboards/admin-web/src/lib/product-context.tsx
index fb1b1299..c69b6d81 100644
--- a/dashboards/admin-web/src/lib/product-context.tsx
+++ b/dashboards/admin-web/src/lib/product-context.tsx
@@ -1,9 +1,10 @@
'use client';
-import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
+import { createContext, useContext, useState, useCallback, useEffect, type ReactNode } from 'react';
import { KNOWN_PRODUCTS, PRODUCT_ID } from '@/lib/product-constants';
const STORAGE_KEY = 'admin_selected_product';
+const PRODUCT_CHANGED_EVENT = 'admin:product-changed';
interface ProductContextValue {
productId: string;
@@ -22,10 +23,24 @@ function getInitialProduct(): string {
export function ProductProvider({ children }: { children: ReactNode }) {
const [productId, setProductIdState] = useState(getInitialProduct);
+ useEffect(() => {
+ function syncSelectedProduct() {
+ setProductIdState(getInitialProduct());
+ }
+
+ window.addEventListener(PRODUCT_CHANGED_EVENT, syncSelectedProduct);
+ window.addEventListener('storage', syncSelectedProduct);
+ return () => {
+ window.removeEventListener(PRODUCT_CHANGED_EVENT, syncSelectedProduct);
+ window.removeEventListener('storage', syncSelectedProduct);
+ };
+ }, []);
+
const setProductId = useCallback((id: string) => {
setProductIdState(id);
if (typeof window !== 'undefined') {
localStorage.setItem(STORAGE_KEY, id);
+ window.dispatchEvent(new Event(PRODUCT_CHANGED_EVENT));
}
}, []);