onairos 2.2.0 → 2.3.0

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.
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useState } from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
 
4
4
  export default function IndividualConnection({
@@ -13,35 +13,165 @@ export default function IndividualConnection({
13
13
  onCheckboxChange,
14
14
  }) {
15
15
  return (
16
- <div className="bg-white rounded-lg p-4 shadow border border-gray-200">
17
- <div className="flex items-center justify-between">
18
- <div className="flex items-center space-x-4">
19
- <div className="group">
16
+ <div className={`relative p-4 sm:p-6 rounded-lg overflow-hidden mb-4 transition-all duration-300 border-2 ${
17
+ !active
18
+ ? 'bg-gray-50 border-gray-200 opacity-60'
19
+ : isChecked
20
+ ? 'bg-green-50 border-green-400 shadow-lg transform scale-[1.02] ring-2 ring-green-200'
21
+ : 'bg-white border-gray-300 hover:border-blue-300 hover:shadow-md'
22
+ }`}>
23
+
24
+ {/* Selection Status Indicator */}
25
+ {isChecked && active && (
26
+ <div className="absolute top-2 right-2 w-8 h-8 bg-green-500 rounded-full flex items-center justify-center animate-pulse shadow-lg">
27
+ <svg className="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
28
+ <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
29
+ </svg>
30
+ </div>
31
+ )}
32
+
33
+ <div className="relative">
34
+ {/* Header with title and status */}
35
+ <div className="flex items-center justify-between mb-4">
36
+ <div className="flex items-center space-x-4">
37
+ {/* Data type icon */}
38
+ <div className={`w-12 h-12 rounded-full flex items-center justify-center text-2xl transition-all duration-300 ${
39
+ !active
40
+ ? 'bg-gray-200 text-gray-400'
41
+ : isChecked
42
+ ? 'bg-green-500 text-white shadow-lg'
43
+ : 'bg-blue-500 text-white'
44
+ }`}>
45
+ {title === "Avatar" ? "👤" :
46
+ title === "Traits" ? "🧠" :
47
+ title === "Personality" ? "🎭" : "📊"}
48
+ </div>
49
+
20
50
  <div>
51
+ <h3 className={`text-xl font-bold ${
52
+ !active ? 'text-gray-400' : isChecked ? 'text-green-700' : 'text-gray-800'
53
+ }`}>
54
+ {title === "Avatar" ? 'Avatar' :
55
+ title === "Traits" ? 'Personality Traits' :
56
+ title === "Personality" ? 'Personality Model' :
57
+ title}
58
+ </h3>
59
+
60
+ {/* Status badge */}
61
+ <div className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-semibold ${
62
+ !active
63
+ ? 'bg-red-100 text-red-700'
64
+ : isChecked
65
+ ? 'bg-green-100 text-green-700'
66
+ : 'bg-blue-100 text-blue-700'
67
+ }`}>
68
+ {!active ? '❌ Not Available' : isChecked ? '✅ SELECTED' : '⏳ Available'}
69
+ </div>
70
+ </div>
71
+ </div>
72
+ </div>
73
+
74
+ {/* Enhanced Checkbox */}
75
+ <div className="flex items-center justify-center mb-4">
76
+ <label className="flex items-center cursor-pointer">
77
+ <div className="relative">
21
78
  <input
22
- disabled={!active}
23
79
  type="checkbox"
24
80
  checked={isChecked}
81
+ disabled={!active}
25
82
  onChange={(e) => onCheckboxChange(e.target.checked)}
83
+ className="sr-only" // Hide default checkbox
26
84
  />
85
+
86
+ {/* Custom checkbox appearance */}
87
+ <div className={`
88
+ w-8 h-8 border-3 rounded-lg flex items-center justify-center transition-all duration-200
89
+ ${!active
90
+ ? 'border-gray-300 bg-gray-100 cursor-not-allowed opacity-50'
91
+ : isChecked
92
+ ? 'border-green-500 bg-green-500 shadow-xl transform scale-110'
93
+ : 'border-gray-400 bg-white hover:border-green-400 hover:bg-green-50 hover:scale-105'
94
+ }
95
+ `}>
96
+ {/* Checkmark icon */}
97
+ {isChecked && (
98
+ <svg
99
+ className="w-6 h-6 text-white animate-bounce"
100
+ fill="currentColor"
101
+ viewBox="0 0 20 20"
102
+ >
103
+ <path
104
+ fillRule="evenodd"
105
+ d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
106
+ clipRule="evenodd"
107
+ />
108
+ </svg>
109
+ )}
110
+ </div>
27
111
  </div>
28
- </div>
112
+
113
+ {/* Selection status text */}
114
+ <span className={`ml-4 text-lg font-bold transition-colors duration-200 ${
115
+ !active
116
+ ? 'text-gray-400'
117
+ : isChecked
118
+ ? 'text-green-600'
119
+ : 'text-gray-700'
120
+ }`}>
121
+ {!active
122
+ ? 'Not available'
123
+ : isChecked
124
+ ? '✓ SELECTED FOR SHARING'
125
+ : 'Click to select'
126
+ }
127
+ </span>
128
+ </label>
29
129
  </div>
30
130
 
31
- <div className="flex items-center">
32
- {/* Optional icons or additional UI elements */}
131
+ {/* Status message for disabled items */}
132
+ <div className="flex items-center justify-center mb-4">
133
+ {!active && (
134
+ <div className="flex items-center space-x-2 text-sm text-red-700 bg-red-100 px-4 py-3 rounded-lg border-2 border-red-300">
135
+ <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
136
+ <path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
137
+ </svg>
138
+ <span className="font-bold">
139
+ {`No ${title} data available to share`}
140
+ </span>
141
+ </div>
142
+ )}
143
+
144
+ {isChecked && active && (
145
+ <div className="flex items-center space-x-2 text-sm text-green-700 bg-green-100 px-4 py-3 rounded-lg border-2 border-green-300 animate-pulse">
146
+ <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
147
+ <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
148
+ </svg>
149
+ <span className="font-bold">
150
+ THIS DATA WILL BE SHARED
151
+ </span>
152
+ </div>
153
+ )}
33
154
  </div>
34
155
 
156
+ {/* Description and rewards with enhanced styling */}
35
157
  {descriptions && title !== "Avatar" && (
36
- <p className="text-sm font-medium text-gray-900 dark:text-gray-300">
37
- Intent: {descriptions}
38
- </p>
158
+ <div className="mb-3 p-3 bg-blue-50 rounded-lg border border-blue-200">
159
+ <p className={`text-sm font-semibold ${
160
+ !active ? 'text-gray-400' : 'text-blue-800'
161
+ }`}>
162
+ <span className="text-blue-600">🎯 Intent:</span> {descriptions}
163
+ </p>
164
+ </div>
39
165
  )}
40
166
 
41
167
  {rewards && (
42
- <p className="text-sm font-medium text-gray-900 dark:text-gray-300">
43
- Rewards: {rewards}
44
- </p>
168
+ <div className="mb-3 p-3 bg-yellow-50 rounded-lg border border-yellow-200">
169
+ <p className={`text-sm font-semibold ${
170
+ !active ? 'text-gray-400' : 'text-yellow-800'
171
+ }`}>
172
+ <span className="text-yellow-600">🎁 Rewards:</span> {rewards}
173
+ </p>
174
+ </div>
45
175
  )}
46
176
  </div>
47
177
  </div>
@@ -11,20 +11,76 @@ function IndividualConnection(props) {
11
11
  'Persona';
12
12
 
13
13
  return (
14
- <div className={`relative p-4 sm:p-6 rounded-sm overflow-hidden mb-8 ${
15
- isDisabled ? 'bg-gray-100' : 'bg-indigo-200'
14
+ <div className={`relative p-4 sm:p-6 rounded-lg overflow-hidden mb-4 transition-all duration-300 border-2 ${
15
+ isDisabled
16
+ ? 'bg-gray-50 border-gray-200 opacity-60'
17
+ : selected
18
+ ? 'bg-green-50 border-green-400 shadow-lg transform scale-[1.02]'
19
+ : 'bg-white border-gray-300 hover:border-blue-300 hover:shadow-md'
16
20
  }`}>
21
+
22
+ {/* Selection Status Indicator */}
23
+ {selected && !isDisabled && (
24
+ <div className="absolute top-2 right-2 w-6 h-6 bg-green-500 rounded-full flex items-center justify-center animate-pulse">
25
+ <svg className="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 20 20">
26
+ <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
27
+ </svg>
28
+ </div>
29
+ )}
30
+
17
31
  <div className="relative">
18
- <div className="flex-center">
32
+ {/* Header with title and status */}
33
+ <div className="flex items-center justify-between mb-4">
34
+ <div className="flex items-center space-x-3">
35
+ {/* Data type icon */}
36
+ <div className={`w-10 h-10 rounded-full flex items-center justify-center text-xl ${
37
+ isDisabled
38
+ ? 'bg-gray-200 text-gray-400'
39
+ : selected
40
+ ? 'bg-green-500 text-white'
41
+ : 'bg-blue-500 text-white'
42
+ }`}>
43
+ {props.title === "Avatar" ? "👤" :
44
+ props.title === "Traits" ? "🧠" :
45
+ props.title === "Personality" ? "🎭" : "📊"}
46
+ </div>
47
+
48
+ <div>
49
+ <h3 className={`text-lg font-semibold ${
50
+ isDisabled ? 'text-gray-400' : selected ? 'text-green-700' : 'text-gray-800'
51
+ }`}>
52
+ {Insight}
53
+ </h3>
54
+
55
+ {/* Status badge */}
56
+ <div className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
57
+ isDisabled
58
+ ? 'bg-red-100 text-red-600'
59
+ : selected
60
+ ? 'bg-green-100 text-green-700'
61
+ : 'bg-blue-100 text-blue-700'
62
+ }`}>
63
+ {isDisabled ? '❌ Not Available' : selected ? '✅ Selected' : '⏳ Available'}
64
+ </div>
65
+ </div>
66
+ </div>
67
+ </div>
68
+
69
+ {/* Checkbox with enhanced styling */}
70
+ <div className="flex items-center justify-center mb-4">
19
71
  <Box
20
72
  active={props.active}
21
73
  onSelectionChange={(isSelected) => {
22
74
  setSelected(isSelected);
23
- props.onSelectionChange(isSelected);
24
- if (isSelected) {
25
- props.changeGranted(1);
26
- } else {
27
- props.changeGranted(-1);
75
+ if (props.onSelectionChange) {
76
+ props.onSelectionChange(isSelected);
77
+ }
78
+ if (props.changeGranted) {
79
+ if (isSelected) {
80
+ props.changeGranted(1);
81
+ } else {
82
+ props.changeGranted(-1);
83
+ }
28
84
  }
29
85
  }}
30
86
  disabled={isDisabled}
@@ -32,31 +88,54 @@ function IndividualConnection(props) {
32
88
  number={props.number + 1}
33
89
  type="Test"
34
90
  title={props.title}
91
+ changeGranted={props.changeGranted}
35
92
  />
36
93
  </div>
37
94
 
38
- <div className="flex items-center mt-2">
95
+ {/* Status message for disabled items */}
96
+ <div className="flex items-center justify-center mb-3">
39
97
  {isDisabled && (
40
- <span className="text-sm text-red-500 font-medium">
41
- {`No ${props.title} data available to share`}
42
- </span>
98
+ <div className="flex items-center space-x-2 text-sm text-red-600 bg-red-50 px-3 py-2 rounded-lg border border-red-200">
99
+ <svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
100
+ <path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
101
+ </svg>
102
+ <span className="font-medium">
103
+ {`No ${props.title} data available to share`}
104
+ </span>
105
+ </div>
106
+ )}
107
+
108
+ {selected && !isDisabled && (
109
+ <div className="flex items-center space-x-2 text-sm text-green-600 bg-green-50 px-3 py-2 rounded-lg border border-green-200">
110
+ <svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
111
+ <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
112
+ </svg>
113
+ <span className="font-medium">
114
+ This data will be shared
115
+ </span>
116
+ </div>
43
117
  )}
44
118
  </div>
45
119
 
120
+ {/* Description and rewards */}
46
121
  {props.descriptions && props.title !== "Avatar" && (
47
- <p className={`text-sm font-medium ${
48
- isDisabled ? 'text-gray-500' : 'text-gray-900'
49
- } dark:text-gray-300`}>
50
- Intent: {props.descriptions}
51
- </p>
122
+ <div className="mb-2">
123
+ <p className={`text-sm font-medium ${
124
+ isDisabled ? 'text-gray-400' : 'text-gray-700'
125
+ }`}>
126
+ <span className="text-blue-600 font-semibold">Intent:</span> {props.descriptions}
127
+ </p>
128
+ </div>
52
129
  )}
53
130
 
54
131
  {props.rewards && (
55
- <p className={`text-sm font-medium ${
56
- isDisabled ? 'text-gray-500' : 'text-gray-900'
57
- } dark:text-gray-300`}>
58
- Rewards: {props.rewards}
59
- </p>
132
+ <div className="mb-2">
133
+ <p className={`text-sm font-medium ${
134
+ isDisabled ? 'text-gray-400' : 'text-gray-700'
135
+ }`}>
136
+ <span className="text-green-600 font-semibold">Rewards:</span> {props.rewards}
137
+ </p>
138
+ </div>
60
139
  )}
61
140
  </div>
62
141
  </div>
@@ -0,0 +1,283 @@
1
+ /**
2
+ * Laravel Blade Integration Example Test
3
+ *
4
+ * This test simulates a real Laravel application using Blade templates
5
+ * with Onairos integration.
6
+ */
7
+
8
+ import { describe, test, expect, beforeEach } from 'vitest';
9
+ import { JSDOM } from 'jsdom';
10
+ import { initializeOnairosForBlade, createOnairosButton } from '../../../src/laravel/blade-helpers.js';
11
+
12
+ describe('Laravel Blade Real-World Example', () => {
13
+ let dom;
14
+ let window;
15
+ let document;
16
+
17
+ beforeEach(() => {
18
+ // Create a DOM that mimics a Laravel Blade template
19
+ dom = new JSDOM(`
20
+ <!DOCTYPE html>
21
+ <html lang="en">
22
+ <head>
23
+ <meta charset="UTF-8">
24
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
25
+ <title>Laravel App with Onairos</title>
26
+ <meta name="csrf-token" content="fake-csrf-token">
27
+ </head>
28
+ <body>
29
+ <div class="container">
30
+ <nav class="navbar">
31
+ <h1>My Laravel App</h1>
32
+ </nav>
33
+
34
+ <main class="content">
35
+ <div class="dashboard">
36
+ <h2>User Dashboard</h2>
37
+ <p>Connect your social accounts to enhance your experience:</p>
38
+
39
+ <!-- Onairos button will be inserted here -->
40
+ <div id="social-connect-button" class="mt-4"></div>
41
+
42
+ <div class="user-profile mt-6">
43
+ <h3>Profile Enhancement</h3>
44
+ <p>Let AI analyze your data for better recommendations:</p>
45
+
46
+ <!-- Another Onairos button for advanced features -->
47
+ <div id="profile-enhancement-button" class="mt-2"></div>
48
+ </div>
49
+ </div>
50
+ </main>
51
+ </div>
52
+ </body>
53
+ </html>
54
+ `);
55
+
56
+ window = dom.window;
57
+ document = dom.window.document;
58
+
59
+ // Set globals for testing
60
+ global.window = window;
61
+ global.document = document;
62
+ global.navigator = window.navigator;
63
+ });
64
+
65
+ test('should integrate Onairos into Laravel dashboard page', () => {
66
+ // 1. Initialize Onairos as would be done in Laravel app.js
67
+ initializeOnairosForBlade({
68
+ testMode: true,
69
+ baseUrl: 'https://api2.onairos.uk',
70
+ autoDetectMobile: true,
71
+ globalStyles: true
72
+ });
73
+
74
+ // 2. Verify initialization worked
75
+ expect(window.OnairosConfig).toBeDefined();
76
+ expect(window.OnairosConfig.testMode).toBe(true);
77
+ expect(document.getElementById('onairos-styles')).not.toBeNull();
78
+
79
+ // 3. Create social connect button (as would be in Blade template script)
80
+ createOnairosButton('social-connect-button', {
81
+ requestData: ['email', 'profile', 'social'],
82
+ webpageName: 'Laravel Dashboard',
83
+ buttonType: 'pill',
84
+ textColor: 'white',
85
+ onComplete: function(result) {
86
+ console.log('Social connection completed:', result);
87
+ // In real Laravel app, this might update the UI or make AJAX calls
88
+ }
89
+ });
90
+
91
+ // 4. Create profile enhancement button
92
+ createOnairosButton('profile-enhancement-button', {
93
+ requestData: {
94
+ basic: { type: "basic", reward: "10 tokens" },
95
+ personality: { type: "personality", reward: "25 tokens" },
96
+ preferences: { type: "preferences", reward: "15 tokens" }
97
+ },
98
+ webpageName: 'Laravel Profile Enhancement',
99
+ buttonType: 'rounded',
100
+ textColor: 'black',
101
+ onComplete: function(result) {
102
+ console.log('Profile enhancement completed:', result);
103
+ }
104
+ });
105
+
106
+ // 5. Verify both buttons were created successfully
107
+ const socialButton = document.getElementById('social-connect-button-btn');
108
+ const profileButton = document.getElementById('profile-enhancement-button-btn');
109
+
110
+ expect(socialButton).not.toBeNull();
111
+ expect(profileButton).not.toBeNull();
112
+
113
+ // 6. Verify button configurations
114
+ const socialConfig = JSON.parse(socialButton.getAttribute('data-onairos-config'));
115
+ const profileConfig = JSON.parse(profileButton.getAttribute('data-onairos-config'));
116
+
117
+ expect(socialConfig.requestData).toEqual(['email', 'profile', 'social']);
118
+ expect(socialConfig.webpageName).toBe('Laravel Dashboard');
119
+ expect(socialConfig.buttonType).toBe('pill');
120
+
121
+ expect(profileConfig.requestData).toHaveProperty('basic');
122
+ expect(profileConfig.requestData).toHaveProperty('personality');
123
+ expect(profileConfig.webpageName).toBe('Laravel Profile Enhancement');
124
+ expect(profileConfig.buttonType).toBe('rounded');
125
+
126
+ // 7. Verify styling was applied
127
+ expect(socialButton.className).toContain('onairos-btn-pill');
128
+ expect(profileButton.className).toContain('onairos-btn-rounded');
129
+ });
130
+
131
+ test('should handle Laravel CSRF protection', () => {
132
+ // Laravel apps typically have CSRF tokens in meta tags
133
+ const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
134
+ expect(csrfToken).toBe('fake-csrf-token');
135
+
136
+ // Initialize Onairos with CSRF handling
137
+ initializeOnairosForBlade({
138
+ csrfToken: csrfToken,
139
+ testMode: true
140
+ });
141
+
142
+ expect(window.OnairosConfig.csrfToken).toBe('fake-csrf-token');
143
+ });
144
+
145
+ test('should support Laravel environment variables pattern', () => {
146
+ // Mock Laravel Vite environment variables
147
+ global.import = {
148
+ meta: {
149
+ env: {
150
+ DEV: false,
151
+ PROD: true,
152
+ VITE_APP_NAME: 'Laravel App',
153
+ VITE_ONAIROS_API_KEY: 'prod-api-key',
154
+ VITE_ONAIROS_TEST_MODE: 'false'
155
+ }
156
+ }
157
+ };
158
+
159
+ // Initialize using Laravel environment pattern
160
+ initializeOnairosForBlade({
161
+ testMode: global.import.meta.env.VITE_ONAIROS_TEST_MODE === 'true',
162
+ apiKey: global.import.meta.env.VITE_ONAIROS_API_KEY,
163
+ appName: global.import.meta.env.VITE_APP_NAME
164
+ });
165
+
166
+ expect(window.OnairosConfig.testMode).toBe(false);
167
+ expect(window.OnairosConfig.apiKey).toBe('prod-api-key');
168
+ expect(window.OnairosConfig.appName).toBe('Laravel App');
169
+ });
170
+
171
+ test('should handle mobile responsive behavior in Laravel context', () => {
172
+ // Mock mobile user agent (iPhone)
173
+ Object.defineProperty(window.navigator, 'userAgent', {
174
+ writable: true,
175
+ value: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15'
176
+ });
177
+
178
+ // Mock mobile viewport
179
+ Object.defineProperty(window, 'innerWidth', {
180
+ writable: true,
181
+ value: 375
182
+ });
183
+
184
+ initializeOnairosForBlade({
185
+ autoDetectMobile: true
186
+ });
187
+
188
+ expect(window.OnairosUtils.isMobile).toBe(true);
189
+
190
+ // Create a button and verify mobile-specific behavior
191
+ createOnairosButton('mobile-test-button', {
192
+ requestData: ['email'],
193
+ webpageName: 'Mobile Laravel App'
194
+ });
195
+
196
+ const button = document.getElementById('mobile-test-button-btn');
197
+ expect(button).not.toBeNull();
198
+
199
+ // In mobile context, button should have mobile-optimized styling
200
+ const styles = window.getComputedStyle ? window.getComputedStyle(button) : {};
201
+ // Note: JSDOM doesn't compute styles, but we can verify the classes are applied
202
+ expect(button.className).toContain('onairos-btn');
203
+ });
204
+
205
+ test('should simulate Laravel Ajax integration', async () => {
206
+ // Mock Laravel AJAX setup (similar to what Laravel includes by default)
207
+ window.axios = {
208
+ defaults: {
209
+ headers: {
210
+ common: {
211
+ 'X-CSRF-TOKEN': 'fake-csrf-token',
212
+ 'X-Requested-With': 'XMLHttpRequest'
213
+ }
214
+ }
215
+ },
216
+ post: vi.fn(() => Promise.resolve({ data: { success: true } }))
217
+ };
218
+
219
+ initializeOnairosForBlade({
220
+ testMode: true,
221
+ onAuthComplete: async function(result) {
222
+ // Simulate Laravel AJAX call to store Onairos data
223
+ try {
224
+ const response = await window.axios.post('/onairos/callback', {
225
+ user_hash: result.userHash,
226
+ connections: result.connectedAccounts,
227
+ data_types: result.requestData
228
+ });
229
+
230
+ if (response.data.success) {
231
+ console.log('Laravel backend updated successfully');
232
+ }
233
+ } catch (error) {
234
+ console.error('Laravel integration error:', error);
235
+ }
236
+ }
237
+ });
238
+
239
+ // Trigger a simulated auth completion
240
+ const mockResult = {
241
+ userHash: 'test-hash-123',
242
+ connectedAccounts: ['youtube', 'linkedin'],
243
+ requestData: ['email', 'profile']
244
+ };
245
+
246
+ // Simulate the callback
247
+ if (window.OnairosConfig.onAuthComplete) {
248
+ await window.OnairosConfig.onAuthComplete(mockResult);
249
+ }
250
+
251
+ // Verify Laravel AJAX was called
252
+ expect(window.axios.post).toHaveBeenCalledWith('/onairos/callback', {
253
+ user_hash: 'test-hash-123',
254
+ connections: ['youtube', 'linkedin'],
255
+ data_types: ['email', 'profile']
256
+ });
257
+ });
258
+
259
+ test('should work with Laravel Blade @vite directive pattern', () => {
260
+ // Simulate what happens when Laravel's @vite directive loads assets
261
+ const viteScript = document.createElement('script');
262
+ viteScript.type = 'module';
263
+ viteScript.textContent = `
264
+ // Simulate Vite module loading
265
+ import { initializeOnairosForBlade } from '/resources/js/onairos-setup.js';
266
+
267
+ // Auto-initialize when loaded via Vite
268
+ document.addEventListener('DOMContentLoaded', () => {
269
+ initializeOnairosForBlade({
270
+ testMode: import.meta.env.DEV,
271
+ baseUrl: import.meta.env.VITE_ONAIROS_BASE_URL
272
+ });
273
+ });
274
+ `;
275
+
276
+ document.head.appendChild(viteScript);
277
+
278
+ // Verify script was added (simulating Vite asset injection)
279
+ const addedScript = document.querySelector('script[type="module"]');
280
+ expect(addedScript).not.toBeNull();
281
+ expect(addedScript.textContent).toContain('initializeOnairosForBlade');
282
+ });
283
+ });