diff --git a/docs/content/docs/1.guides/1.script-triggers.md b/docs/content/docs/1.guides/1.script-triggers.md index bdf30482..2f6ae980 100644 --- a/docs/content/docs/1.guides/1.script-triggers.md +++ b/docs/content/docs/1.guides/1.script-triggers.md @@ -1,52 +1,56 @@ --- -title: Triggering Script Loading +title: Script Triggers +description: Control when scripts load with Nuxt Scripts' flexible trigger system. --- -Nuxt Scripts provides several ways to trigger the loading of scripts. +Nuxt Scripts provides a flexible trigger system to control when scripts load, helping you optimize performance by loading scripts at the right moment for your users. -::code-group +## Default: onNuxtReady -```ts [useScript - Ref] -import { useTimeout } from '@vueuse/core' +By default, scripts use the `onNuxtReady` trigger, providing "idle loading" behavior where scripts load only after the page is fully interactive. This minimizes impact on Core Web Vitals and user experience. -const { ready } = useTimeout(3000) -useScript({ - src: 'https://example.com/script.js', -}, { - // load however you like! - trigger: ready, // refs supported +The `onNuxtReady` trigger ensures scripts load: +- After Nuxt hydration is complete +- When the browser is idle and the main thread is available +- Without blocking critical page rendering or user interactions + +```ts +// Default behavior - explicit for clarity +useScript('https://widget.intercom.io/widget/abc123', { + trigger: 'onNuxtReady' }) -``` -```ts [useScript - Computed] -const route = useRoute() -useScript({ - src: 'https://example.com/script.js', -}, { - // only if route has a specific query - trigger: computed(() => !!route.query.affiliateId), +// Registry scripts also use onNuxtReady by default +useScriptGoogleAnalytics({ + id: 'GA_MEASUREMENT_ID' + // trigger: 'onNuxtReady' is implied }) ``` -```ts [Registry Script] -import { useTimeout } from '@vueuse/core' +You can change this default by modifying the [defaultScriptOptions](/docs/api/nuxt-config#defaultscriptoptions). -const { ready } = useTimeout(3000) -useScriptMetaPixel({ - id: '1234567890', - scriptOptions: { - trigger: ready - } +## Specialized Triggers + +### Idle Timeout + +Use [useScriptTriggerIdleTimeout](/docs/api/use-script-trigger-idle-timeout) to delay script loading for a specified time after Nuxt is ready: + +::code-group + +```ts [Composable] +useScript('https://example.com/analytics.js', { + trigger: useScriptTriggerIdleTimeout({ timeout: 5000 }) }) ``` -```ts [Global Script] +```ts [nuxt.config.ts] export default defineNuxtConfig({ scripts: { - globals: { - myScript: ['https://example.com/script.js', { - // load after page is fully interactive (idle loading) - trigger: 'onNuxtReady' + registry: { + googleAnalytics: [{ + id: 'GA_MEASUREMENT_ID' + }, { + trigger: { idleTimeout: 3000 } }] } } @@ -55,93 +59,93 @@ export default defineNuxtConfig({ :: -## Default Behavior - -By default, scripts are loaded when Nuxt is fully hydrated using the `onNuxtReady` trigger. This provides an "idle loading" behavior where scripts load only after the page is fully interactive, minimizing impact on Core Web Vitals and user experience. - -The `onNuxtReady` trigger ensures scripts load: -- After Nuxt hydration is complete -- When the browser is idle and the main thread is available -- Without blocking critical page rendering or user interactions - -This is more effective than using `defer` or `fetchpriority="low"` attributes alone, as it waits for the application to be fully ready rather than just the HTML parsing to complete. - -You can change this default by modifying the [defaultScriptOptions](/docs/api/nuxt-config#defaultscriptoptions). +### User Interaction -## Idle Loading with onNuxtReady +Use [useScriptTriggerInteraction](/docs/api/use-script-trigger-interaction) to load scripts when users interact with your site: -The `onNuxtReady` trigger is perfect for non-critical scripts like chat widgets, analytics, or marketing tools that should load with minimal performance impact: +::code-group -```ts -// Chat widget - loads after page is fully interactive -useScript('https://widget.intercom.io/widget/abc123', { - trigger: 'onNuxtReady' // default behavior +```ts [Composable] +useScript('https://example.com/chat-widget.js', { + trigger: useScriptTriggerInteraction({ + events: ['scroll', 'click', 'keydown'] + }) }) +``` -// Explicitly using onNuxtReady for clarity -useScriptGoogleAnalytics({ - id: 'GA_MEASUREMENT_ID', - scriptOptions: { - trigger: 'onNuxtReady' +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + scripts: { + globals: { + chatWidget: ['https://widget.example.com/chat.js', { + trigger: { interaction: ['scroll', 'click', 'touchstart'] } + }] + } } }) ``` -This approach ensures your Core Web Vitals remain optimal while still loading necessary third-party scripts. +:: -## Element Event Triggers +### Element Events -The [useScriptTriggerElement](/docs/api/use-script-trigger-element) composable allows you to hook into element events as a way to load script. This is useful for loading scripts when a user interacts with a specific element. +Use [useScriptTriggerElement](/docs/api/use-script-trigger-element) to trigger scripts based on specific element interactions: ```ts -const somethingEl = ref() -const script = useScript({ - src: 'https://example.com/script.js', -}, { +const buttonEl = ref() + +useScript('https://example.com/feature.js', { trigger: useScriptTriggerElement({ - trigger: 'hover', - el: somethingEl, + trigger: 'visible', // or 'hover', 'click', etc. + el: buttonEl, }) }) ``` -It has support for the following triggers: -- `visible` - Triggered when the element becomes visible in the viewport. -- `mouseover` - Triggered when the element is hovered over. +## Basic Triggers -## Manual Trigger +### Manual Control -The `manual` trigger allows you to manually trigger the loading of a script. This gives you complete -control over when the script is loaded. +Use `manual` trigger for complete control over script loading: ```ts const { load } = useScript('https://example.com/script.js', { trigger: 'manual' }) -// ... -load() + +// Load when you decide +await load() ``` -## Promise +### Custom Logic -You can use a promise to trigger the loading of a script. This is useful for any other custom trigger you might want to use. +Use reactive values or promises for custom loading logic: -```ts -const myScript = useScript('/script.js', { - // load after 3 seconds - trigger: new Promise(resolve => setTimeout(resolve, 3000)) +::code-group + +```ts [Ref] +const shouldLoad = ref(false) + +useScript('https://example.com/script.js', { + trigger: shouldLoad }) + +// Trigger loading +shouldLoad.value = true ``` -## Ref +```ts [Computed] +const route = useRoute() -You can use a ref to trigger the loading of a script. This is useful for any other custom trigger you might want to use. +useScript('https://example.com/script.js', { + trigger: computed(() => !!route.query.affiliateId) +}) +``` -```ts -const myRef = ref(false) -const myScript = useScript('/script.js', { - trigger: myRef +```ts [Promise] +useScript('https://example.com/script.js', { + trigger: new Promise(resolve => setTimeout(resolve, 3000)) }) -// ... -myRef.value = true ``` + +:: diff --git a/docs/content/docs/3.api/3.use-script-trigger-idle-timeout.md b/docs/content/docs/3.api/3.use-script-trigger-idle-timeout.md new file mode 100644 index 00000000..066ff539 --- /dev/null +++ b/docs/content/docs/3.api/3.use-script-trigger-idle-timeout.md @@ -0,0 +1,132 @@ +--- +title: useScriptTriggerIdleTimeout +description: API documentation for the useScriptTriggerIdleTimeout function. +links: + - label: Source + icon: i-simple-icons-github + to: https://github.com/nuxt/scripts/blob/main/src/runtime/composables/useScriptTriggerIdleTimeout.ts + size: xs +--- + +Create a trigger that loads a script after an idle timeout once Nuxt is ready. This is useful for non-critical scripts that should load with a delay to further minimize impact on initial page performance. + +## Signature + +```ts +function useScriptTriggerIdleTimeout(options: IdleTimeoutScriptTriggerOptions): Promise +``` + +## Arguments + +```ts +export interface IdleTimeoutScriptTriggerOptions { + /** + * The timeout in milliseconds to wait before loading the script. + */ + timeout: number +} +``` + +## Returns + +A promise that resolves to `true` when the timeout completes and the script should be loaded, or `false` if the trigger is cancelled. + +## Nuxt Config Usage + +For convenience, you can use this trigger directly in your `nuxt.config` without calling the composable explicitly: + +::code-group + +```ts [Registry Script] +export default defineNuxtConfig({ + scripts: { + registry: { + googleAnalytics: [{ + id: 'GA_MEASUREMENT_ID' + }, { + trigger: { idleTimeout: 3000 } // Load 3 seconds after Nuxt ready + }] + } + } +}) +``` + +```ts [Global Script] +export default defineNuxtConfig({ + scripts: { + globals: { + chatWidget: ['https://widget.example.com/chat.js', { + trigger: { idleTimeout: 5000 } // Load 5 seconds after Nuxt ready + }] + } + } +}) +``` + +:: + +## Examples + +### Basic Usage + +Load a script 5 seconds after Nuxt is ready: + +```ts +const script = useScript({ + src: 'https://example.com/analytics.js', +}, { + trigger: useScriptTriggerIdleTimeout({ timeout: 5000 }) +}) +``` + +### Delayed Analytics Loading + +Perfect for analytics scripts that don't need to load immediately: + +```vue + +``` + +### Progressive Enhancement + +Load enhancement scripts with a delay to prioritize critical resources: + +```vue + +``` + +## Best Practices + +- **Use appropriate timeouts**: 3-5 seconds for analytics, 5-10 seconds for widgets, longer for non-essential features +- **Consider user behavior**: Shorter timeouts for high-engagement pages, longer for content-focused pages +- **Monitor performance**: Ensure delayed loading actually improves your Core Web Vitals +- **Combine with other triggers**: Use alongside interaction triggers for optimal user experience \ No newline at end of file diff --git a/docs/content/docs/3.api/3.use-script-trigger-interaction.md b/docs/content/docs/3.api/3.use-script-trigger-interaction.md new file mode 100644 index 00000000..cd601e9b --- /dev/null +++ b/docs/content/docs/3.api/3.use-script-trigger-interaction.md @@ -0,0 +1,201 @@ +--- +title: useScriptTriggerInteraction +description: API documentation for the useScriptTriggerInteraction function. +links: + - label: Source + icon: i-simple-icons-github + to: https://github.com/nuxt/scripts/blob/main/src/runtime/composables/useScriptTriggerInteraction.ts + size: xs +--- + +Create a trigger that loads a script when any of the specified user interaction events occur. This is perfect for loading scripts only when users actually interact with your site, providing excellent performance optimization. + +## Signature + +```ts +function useScriptTriggerInteraction(options: InteractionScriptTriggerOptions): Promise +``` + +## Arguments + +```ts +export interface InteractionScriptTriggerOptions { + /** + * The interaction events to listen for. + */ + events: string[] + /** + * The element to listen for events on. + * @default document.documentElement + */ + target?: EventTarget | null +} +``` + +## Returns + +A promise that resolves to `true` when any of the specified interaction events occur and the script should be loaded, or `false` if the trigger is cancelled. + +## Nuxt Config Usage + +For convenience, you can use this trigger directly in your `nuxt.config` without calling the composable explicitly: + +::code-group + +```ts [Registry Script] +export default defineNuxtConfig({ + scripts: { + registry: { + googleAnalytics: [{ + id: 'GA_MEASUREMENT_ID' + }, { + trigger: { interaction: ['scroll', 'click', 'keydown'] } + }] + } + } +}) +``` + +```ts [Global Script] +export default defineNuxtConfig({ + scripts: { + globals: { + chatWidget: ['https://widget.example.com/chat.js', { + trigger: { interaction: ['scroll', 'click', 'touchstart'] } + }] + } + } +}) +``` + +:: + +## Common Event Types + +Here are some commonly used interaction events: + +- **Mouse events**: `click`, `mousedown`, `mouseup`, `mouseover`, `mouseenter`, `mouseleave` +- **Touch events**: `touchstart`, `touchend`, `touchmove` +- **Keyboard events**: `keydown`, `keyup`, `keypress` +- **Scroll events**: `scroll`, `wheel` +- **Focus events**: `focus`, `blur`, `focusin`, `focusout` + +## Examples + +### Basic Usage + +Load a script when the user scrolls, clicks, or presses a key: + +```ts +const script = useScript({ + src: 'https://example.com/chat-widget.js', +}, { + trigger: useScriptTriggerInteraction({ + events: ['scroll', 'click', 'keydown'] + }) +}) +``` + +### Analytics on First Interaction + +Load analytics only when users actually interact with your site: + +```vue + +``` + +### Specific Element Targeting + +Listen for events on a specific element: + +```vue + + + +``` + +### Progressive Feature Loading + +Load features based on different interaction types: + +```vue + +``` + +### Mobile-Optimized Loading + +Use touch events for mobile-specific optimizations: + +```vue + +``` + +## Best Practices + +- **Start with common events**: Use `['scroll', 'click', 'keydown']` as a good default for most use cases +- **Consider mobile users**: Include touch events like `touchstart` for mobile optimization +- **Target specific elements**: Use the `target` option to listen only on relevant parts of your page +- **Combine events wisely**: Don't overload with too many event types - focus on meaningful interactions +- **Test performance impact**: Verify that interaction-based loading actually improves your metrics +- **Fallback considerations**: Consider what happens if users don't interact (e.g., combine with a timeout trigger) \ No newline at end of file diff --git a/src/runtime/composables/useScriptTriggerIdleTimeout.ts b/src/runtime/composables/useScriptTriggerIdleTimeout.ts new file mode 100644 index 00000000..94d4ff8c --- /dev/null +++ b/src/runtime/composables/useScriptTriggerIdleTimeout.ts @@ -0,0 +1,36 @@ +import { tryOnScopeDispose } from '@vueuse/shared' +import { useTimeoutFn } from '@vueuse/core' +import { onNuxtReady } from 'nuxt/app' + +export interface IdleTimeoutScriptTriggerOptions { + /** + * The timeout in milliseconds to wait before loading the script. + */ + timeout: number +} + +/** + * Create a trigger that loads a script after an idle timeout once Nuxt is ready. + */ +export function useScriptTriggerIdleTimeout(options: IdleTimeoutScriptTriggerOptions): Promise { + if (import.meta.server) { + return new Promise(() => {}) + } + + const { timeout } = options + + return new Promise((resolve) => { + onNuxtReady(() => { + const { start, stop } = useTimeoutFn(() => { + resolve(true) + }, timeout, { immediate: false }) + + start() + + tryOnScopeDispose(() => { + stop() + resolve(false) + }) + }) + }) +} diff --git a/src/runtime/composables/useScriptTriggerInteraction.ts b/src/runtime/composables/useScriptTriggerInteraction.ts new file mode 100644 index 00000000..8af93fea --- /dev/null +++ b/src/runtime/composables/useScriptTriggerInteraction.ts @@ -0,0 +1,57 @@ +import { useEventListener } from '@vueuse/core' +import { tryOnScopeDispose } from '@vueuse/shared' +import { onNuxtReady } from 'nuxt/app' + +export interface InteractionScriptTriggerOptions { + /** + * The interaction events to listen for. + */ + events: string[] + /** + * The element to listen for events on. + * @default document.documentElement + */ + target?: EventTarget | null +} + +/** + * Create a trigger that loads a script when any of the specified interaction events occur. + */ +export function useScriptTriggerInteraction(options: InteractionScriptTriggerOptions): Promise { + if (import.meta.server) { + return new Promise(() => {}) + } + + const { events, target = document.documentElement } = options + + return new Promise((resolve) => { + onNuxtReady(() => { + if (!target) { + resolve(false) + return + } + + const cleanupFns: Array<() => void> = [] + + // Listen for all specified events + events.forEach((event) => { + const cleanup = useEventListener( + target, + event, + () => { + // Clean up all listeners when any event triggers + cleanupFns.forEach(fn => fn()) + resolve(true) + }, + { once: true, passive: true }, + ) + cleanupFns.push(cleanup) + }) + + tryOnScopeDispose(() => { + cleanupFns.forEach(fn => fn()) + resolve(false) + }) + }) + }) +} diff --git a/src/runtime/types.ts b/src/runtime/types.ts index 2bc85c11..33408705 100644 --- a/src/runtime/types.ts +++ b/src/runtime/types.ts @@ -91,7 +91,7 @@ export type NuxtUseScriptOptions = {}> = _validate?: () => ValiError | null | undefined } -export type NuxtUseScriptOptionsSerializable = Omit & { trigger?: 'client' | 'server' | 'onNuxtReady' } +export type NuxtUseScriptOptionsSerializable = Omit & { trigger?: 'client' | 'server' | 'onNuxtReady' | { idleTimeout: number } | { interaction: string[] } } export type NuxtUseScriptInput = UseScriptInput diff --git a/src/templates.ts b/src/templates.ts index ddd1912d..ee24447c 100644 --- a/src/templates.ts +++ b/src/templates.ts @@ -5,6 +5,22 @@ import type { RegistryScript } from '#nuxt-scripts/types' import { tryUseNuxt } from '@nuxt/kit' import { relative } from 'pathe' +export function resolveTriggerForTemplate(trigger: any): string | null { + if (trigger && typeof trigger === 'object') { + const keys = Object.keys(trigger) + if (keys.length > 1) { + throw new Error(`Trigger object must have exactly one property, received: ${keys.join(', ')}`) + } + if ('idleTimeout' in trigger) { + return `useScriptTriggerIdleTimeout({ timeout: ${trigger.idleTimeout} })` + } + if ('interaction' in trigger) { + return `useScriptTriggerInteraction({ events: ${JSON.stringify(trigger.interaction)} })` + } + } + return null +} + export function templatePlugin(config: Partial, registry: Required[]) { if (Array.isArray(config.globals)) { // convert to object @@ -16,6 +32,9 @@ export function templatePlugin(config: Partial, registry: Require const buildDir = nuxt.options.buildDir const imports = [] const inits = [] + let needsIdleTimeoutImport = false + let needsInteractionImport = false + // for global scripts, we can initialise them script away for (const [k, c] of Object.entries(config.registry || {})) { const importDefinition = registry.find(i => i.import.name === `useScript${k.substring(0, 1).toUpperCase() + k.substring(1)}`) @@ -23,9 +42,22 @@ export function templatePlugin(config: Partial, registry: Require // title case imports.unshift(`import { ${importDefinition.import.name} } from '${relative(buildDir, importDefinition.import.from)}'`) const args = (typeof c !== 'object' ? {} : c) || {} - if (c === 'mock') + if (c === 'mock') { args.scriptOptions = { trigger: 'manual', skipValidation: true } - inits.push(`const ${k} = ${importDefinition.import.name}(${JSON.stringify(args)})`) + } + else if (Array.isArray(c) && c.length === 2 && c[1]?.trigger) { + const triggerResolved = resolveTriggerForTemplate(c[1].trigger) + if (triggerResolved) { + args.scriptOptions = { ...c[1] } as any + // Store the resolved trigger as a string that will be replaced later + if (args.scriptOptions) { + args.scriptOptions.trigger = `__TRIGGER_${triggerResolved}__` as any + } + if (triggerResolved.includes('useScriptTriggerIdleTimeout')) needsIdleTimeoutImport = true + if (triggerResolved.includes('useScriptTriggerInteraction')) needsInteractionImport = true + } + } + inits.push(`const ${k} = ${importDefinition.import.name}(${JSON.stringify(args).replace(/"__TRIGGER_(.*?)__"/g, '$1')})`) } } for (const [k, c] of Object.entries(config.globals || {})) { @@ -33,15 +65,44 @@ export function templatePlugin(config: Partial, registry: Require inits.push(`const ${k} = useScript(${JSON.stringify({ src: c, key: k })}, { use: () => ({ ${k}: window.${k} }) })`) } else if (Array.isArray(c) && c.length === 2) { - inits.push(`const ${k} = useScript(${JSON.stringify({ key: k, ...(typeof c[0] === 'string' ? { src: c[0] } : c[0]) })}, { ...${JSON.stringify(c[1])}, use: () => ({ ${k}: window.${k} }) })`) + const options = c[1] + const triggerResolved = resolveTriggerForTemplate(options?.trigger) + if (triggerResolved) { + if (triggerResolved.includes('useScriptTriggerIdleTimeout')) needsIdleTimeoutImport = true + if (triggerResolved.includes('useScriptTriggerInteraction')) needsInteractionImport = true + const resolvedOptions = { ...options, trigger: `__TRIGGER_${triggerResolved}__` } as any + inits.push(`const ${k} = useScript(${JSON.stringify({ key: k, ...(typeof c[0] === 'string' ? { src: c[0] } : c[0]) })}, { ...${JSON.stringify(resolvedOptions).replace(/"__TRIGGER_(.*?)__"/g, '$1')}, use: () => ({ ${k}: window.${k} }) })`) + } + else { + inits.push(`const ${k} = useScript(${JSON.stringify({ key: k, ...(typeof c[0] === 'string' ? { src: c[0] } : c[0]) })}, { ...${JSON.stringify(c[1])}, use: () => ({ ${k}: window.${k} }) })`) + } } - else { - inits.push(`const ${k} = useScript(${JSON.stringify({ key: k, ...c })}, { use: () => ({ ${k}: window.${k} }) })`) + else if (typeof c === 'object' && c !== null) { + const triggerResolved = resolveTriggerForTemplate((c as any).trigger) + if (triggerResolved) { + if (triggerResolved.includes('useScriptTriggerIdleTimeout')) needsIdleTimeoutImport = true + if (triggerResolved.includes('useScriptTriggerInteraction')) needsInteractionImport = true + const resolvedOptions = { ...c, trigger: `__TRIGGER_${triggerResolved}__` } as any + inits.push(`const ${k} = useScript(${JSON.stringify({ key: k, ...resolvedOptions }).replace(/"__TRIGGER_(.*?)__"/g, '$1')}, { use: () => ({ ${k}: window.${k} }) })`) + } + else { + inits.push(`const ${k} = useScript(${JSON.stringify({ key: k, ...c })}, { use: () => ({ ${k}: window.${k} }) })`) + } } } + // Add conditional imports for trigger composables + const triggerImports = [] + if (needsIdleTimeoutImport) { + triggerImports.push(`import { useScriptTriggerIdleTimeout } from '#nuxt-scripts/composables/useScriptTriggerIdleTimeout'`) + } + if (needsInteractionImport) { + triggerImports.push(`import { useScriptTriggerInteraction } from '#nuxt-scripts/composables/useScriptTriggerInteraction'`) + } + return [ `import { useScript } from '#nuxt-scripts/composables/useScript'`, `import { defineNuxtPlugin } from 'nuxt/app'`, + ...triggerImports, ...imports, '', `export default defineNuxtPlugin({`, diff --git a/test/unit/templates.test.ts b/test/unit/templates.test.ts index eda1c6f3..5ea90c36 100644 --- a/test/unit/templates.test.ts +++ b/test/unit/templates.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import { templatePlugin } from '../../src/templates' +import { templatePlugin, resolveTriggerForTemplate } from '../../src/templates' describe('template plugin file', () => { // global @@ -141,4 +141,102 @@ describe('template plugin file', () => { ]) expect(res).toContain('useScriptStripe([{"id":"test"},{"trigger":"onNuxtReady"}])') }) + + // Test idleTimeout trigger in globals + it('global with idleTimeout trigger', async () => { + const res = templatePlugin({ + globals: { + analytics: ['https://analytics.example.com/script.js', { + trigger: { idleTimeout: 3000 }, + }], + }, + }, []) + expect(res).toContain('import { useScriptTriggerIdleTimeout }') + expect(res).toContain('useScriptTriggerIdleTimeout({ timeout: 3000 })') + }) + + // Test interaction trigger in globals + it('global with interaction trigger', async () => { + const res = templatePlugin({ + globals: { + chatWidget: ['https://chat.example.com/widget.js', { + trigger: { interaction: ['scroll', 'click'] }, + }], + }, + }, []) + expect(res).toContain('import { useScriptTriggerInteraction }') + expect(res).toContain('useScriptTriggerInteraction({ events: [\\"scroll\\",\\"click\\"] })') + }) + + // Test registry with idleTimeout trigger + it('registry with idleTimeout trigger', async () => { + const res = templatePlugin({ + registry: { + googleAnalytics: [ + { id: 'GA_MEASUREMENT_ID' }, + { trigger: { idleTimeout: 5000 } }, + ], + }, + }, [ + { + import: { + name: 'useScriptGoogleAnalytics', + }, + }, + ]) + // Registry scripts pass trigger objects directly, they don't resolve triggers in templates + expect(res).toContain('useScriptGoogleAnalytics([{"id":"GA_MEASUREMENT_ID"},{"trigger":{"idleTimeout":5000}}])') + }) + + // Test both triggers together (should import both) + it('mixed triggers import both composables', async () => { + const res = templatePlugin({ + globals: { + analytics: ['https://analytics.example.com/script.js', { + trigger: { idleTimeout: 3000 }, + }], + chat: ['https://chat.example.com/widget.js', { + trigger: { interaction: ['scroll'] }, + }], + }, + }, []) + expect(res).toContain('import { useScriptTriggerIdleTimeout }') + expect(res).toContain('import { useScriptTriggerInteraction }') + }) +}) + +describe('resolveTriggerForTemplate', () => { + it('should return null for non-object triggers', () => { + expect(resolveTriggerForTemplate('onNuxtReady')).toBe(null) + expect(resolveTriggerForTemplate(null)).toBe(null) + expect(resolveTriggerForTemplate(undefined)).toBe(null) + expect(resolveTriggerForTemplate(42)).toBe(null) + }) + + it('should handle idleTimeout trigger', () => { + const result = resolveTriggerForTemplate({ idleTimeout: 5000 }) + expect(result).toBe('useScriptTriggerIdleTimeout({ timeout: 5000 })') + }) + + it('should handle interaction trigger', () => { + const result = resolveTriggerForTemplate({ interaction: ['scroll', 'click'] }) + expect(result).toBe('useScriptTriggerInteraction({ events: ["scroll","click"] })') + }) + + it('should return null for unknown trigger types', () => { + const result = resolveTriggerForTemplate({ unknownTrigger: 'value' }) + expect(result).toBe(null) + }) + + it('should throw error for multiple trigger properties', () => { + expect(() => { + resolveTriggerForTemplate({ idleTimeout: 3000, interaction: ['click'] }) + }).toThrow('Trigger object must have exactly one property, received: idleTimeout, interaction') + }) + + it('should throw error with correct property names', () => { + expect(() => { + resolveTriggerForTemplate({ idleTimeout: 3000, interaction: ['click'], someOther: 'value' }) + }).toThrow('Trigger object must have exactly one property, received: idleTimeout, interaction, someOther') + }) })