Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ <h1>Gist for Web</h1>
<a href="#" class="button" onClick="logIn()">Log In</a>
<a href="#" class="button" onClick="logOut()">Log Out</a>
<a href="#tooltipDemo" class="button" style="background-color:#8b5cf6;">Tooltip Demo ↓</a>
<a href="#" class="button" style="background-color:#b45309;" onClick="enablePreviewBar()">Enable Preview Bar</a>
</div>
<div class="row docs">
<p>More information can be found on our <a target="_blank" href="https://docs.gist.build">docs</a>, if you have any question you can email us at <a target="_blank" href="mailto:support@gist.build">support@gist.build</a></p>
Expand Down Expand Up @@ -397,6 +398,39 @@ <h4>Active Messages & Display Settings</h4>
setTimeout(refreshActiveMessages, 500);
});

// ─── Preview Bar Demo ─────────────────────────────
function enablePreviewBar() {
var url = new URL(window.location.href);
if (url.searchParams.has('cioPreviewId')) {
alert('Preview bar is already active.');
return;
}
url.searchParams.set('cioPreviewId', 'local-preview-' + Date.now());
window.location.href = url.toString();
}

// When preview bar is active, auto-show a local HTML message with livePreview
(function() {
var params = new URLSearchParams(window.location.search);
if (!params.has('cioPreviewId')) return;

function showPreviewBarMessage() {
Gist.showMessage({
messageId: 'preview-bar-demo',
position: 'center',
properties: {
gist: {
encodedMessageHtml: EncodedTestHTMLMessage,
livePreview: true
}
}
});
}

// Show the message after a short delay to let the SDK initialize
setTimeout(showPreviewBarMessage, 1000);
})();

// ─── Tooltip Demo ─────────────────────────────────
let activeTooltipInstanceId = null;

Expand Down
16 changes: 16 additions & 0 deletions src/managers/message-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,22 @@ describe('message-manager', () => {
expect(mockGist.messageError).toHaveBeenCalledWith(message);
});

it('returns null and emits error for invalid selector instead of throwing', async () => {
const { resolveMessageProperties } = await import('./gist-properties-manager');
vi.mocked(resolveMessageProperties).mockReturnValue(tooltipProperties('[invalid!'));

const message: GistMessage = {
messageId: 'tooltip-invalid',
tooltipPosition: 'bottom',
properties: { gist: { elementId: '[invalid!', tooltipPosition: 'bottom' } },
};

const result = await showMessage(message);

expect(result).toBeNull();
expect(mockGist.messageError).toHaveBeenCalledWith(message);
});

it('allows tooltip when an overlay is already active', async () => {
const { loadTooltipComponent } = await import('./message-component-manager');
const { resolveMessageProperties } = await import('./gist-properties-manager');
Expand Down
26 changes: 7 additions & 19 deletions src/managers/message-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
hideTooltipComponent,
} from './message-component-manager';
import { resolveMessageProperties } from './gist-properties-manager';
import { findElement } from '../utilities/dom';
import { positions, addPageElement } from './page-component-manager';
import { getAllCustomAttributes } from './custom-attribute-manager';
import { checkMessageQueue } from './queue-manager';
Expand Down Expand Up @@ -116,17 +117,11 @@ function showTooltipMessage(
}

// Verify target element exists in the DOM
try {
const targetElement = document.querySelector(targetSelector);
if (!targetElement) {
log(
`Tooltip target element "${targetSelector}" not found for message ${message.messageId}, skipping display`
);
Gist.messageError(message);
return null;
}
} catch {
log(`Invalid tooltip target selector "${targetSelector}" for message ${message.messageId}`);
const targetElement = findElement(targetSelector);
if (!targetElement) {
log(
`Tooltip target element "${targetSelector}" not found for message ${message.messageId}, skipping display`
);
Gist.messageError(message);
return null;
}
Expand Down Expand Up @@ -375,14 +370,7 @@ async function handleGistEvents(e: MessageEvent): Promise<void> {
(currentMessage.properties?.gist?.elementId as string | undefined) ||
currentMessage.elementId ||
undefined;
let targetFound = false;
try {
targetFound = !!targetSelector && !!document.querySelector(targetSelector);
} catch {
log(
`Invalid tooltip target selector "${targetSelector}" for message ${currentMessage.messageId}`
);
}
const targetFound = !!targetSelector && !!findElement(targetSelector);
if (!targetFound) {
log(
`Tooltip target not found for "${targetSelector}", emitting error and skipping display`
Expand Down
225 changes: 225 additions & 0 deletions src/managers/preview-bar-manager.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import type { DisplaySettings, GistMessage, StepDisplayConfig } from '../types';

const mockGist = vi.hoisted(() => ({
currentMessages: [] as GistMessage[],
dismissMessage: vi.fn(() => Promise.resolve()),
}));

vi.mock('../gist', () => ({ default: mockGist }));
vi.mock('../utilities/log', () => ({ log: vi.fn() }));
vi.mock('./message-manager', () => ({
applyMessageStepChange: vi.fn(),
hideMessageVisually: vi.fn(),
}));
vi.mock('./message-component-manager', () => ({
sendDisplaySettingsToIframe: vi.fn(),
}));
vi.mock('../utilities/message-utils', () => ({
hasDisplayChanged: vi.fn(() => false),
wideOverlayPositions: ['x-gist-top-full', 'x-gist-bottom-full'],
mapOverlayPositionToElementId: vi.fn((pos: string) => pos),
}));
vi.mock('./preview-bar-styles', () => ({
PREVIEW_BAR_CSS: '.gist-pb-toggle-btn { color: white; }',
chevronSvg: vi.fn(() => '<svg></svg>'),
}));
vi.mock('../services/preview-service', () => ({
savePreviewDisplaySettings: vi.fn(() => Promise.resolve({ status: 200 })),
deletePreviewSession: vi.fn(() => Promise.resolve()),
}));
vi.mock('../utilities/preview-mode', () => ({
PREVIEW_PARAM_ID: 'cioPreviewId',
teardownPreview: vi.fn(),
}));

import {
initPreviewBar,
updatePreviewBarMessage,
updatePreviewBarStep,
destroyPreviewBar,
} from './preview-bar-manager';

describe('preview-bar-manager', () => {
beforeEach(() => {
vi.clearAllMocks();
document.body.innerHTML = '';
mockGist.currentMessages = [];
});

afterEach(() => {
destroyPreviewBar();
});

function initBarWithMessage(
steps: StepDisplayConfig[] = [],
displayType: DisplaySettings['displayType'] = 'modal'
): GistMessage {
initPreviewBar();
const message: GistMessage = {
messageId: 'msg-1',
instanceId: 'inst-1',
displaySettings: steps.length > 0 ? (steps as unknown as DisplaySettings) : { displayType },
};
mockGist.currentMessages = [message];
updatePreviewBarMessage(message);
return message;
}

describe('initPreviewBar', () => {
it('creates the preview bar element in the DOM', () => {
initPreviewBar();
expect(document.getElementById('gist-preview-bar')).not.toBeNull();
});

it('injects preview bar styles', () => {
initPreviewBar();
expect(document.getElementById('gist-pb-styles')).not.toBeNull();
});

it('does not duplicate the bar on repeated calls', () => {
initPreviewBar();
initPreviewBar();
expect(document.querySelectorAll('#gist-preview-bar').length).toBe(1);
});
});

describe('display type dropdown', () => {
it('includes tooltip option in display type dropdown', () => {
initBarWithMessage();
const bar = document.getElementById('gist-preview-bar')!;
const select = bar.querySelector<HTMLSelectElement>('.gist-pb-select');
expect(select).not.toBeNull();

const options = Array.from(select!.options).map((o) => o.value);
expect(options).toContain('tooltip');
});

it('includes all four display types: modal, overlay, inline, tooltip', () => {
initBarWithMessage();
const bar = document.getElementById('gist-preview-bar')!;
const selects = bar.querySelectorAll<HTMLSelectElement>('.gist-pb-select');
const displayTypeSelect = selects[0];
const values = Array.from(displayTypeSelect.options).map((o) => o.value);

expect(values).toEqual(['modal', 'overlay', 'inline', 'tooltip']);
});
});

describe('tooltip controls', () => {
it('renders element selector and position controls when display type is tooltip', () => {
initBarWithMessage([], 'tooltip');
const bar = document.getElementById('gist-preview-bar')!;
const labels = Array.from(bar.querySelectorAll('.gist-pb-label')).map((el) => el.textContent);

expect(labels).toContain('Element Selector');
expect(labels).toContain('Position');
});

it('renders tooltip position options: top, bottom, left, right', () => {
initBarWithMessage([], 'tooltip');
const bar = document.getElementById('gist-preview-bar')!;
const selects = bar.querySelectorAll<HTMLSelectElement>('.gist-pb-select');
const positionSelect = Array.from(selects).find((s) =>
Array.from(s.options).some((o) => o.value === 'left')
);
expect(positionSelect).not.toBeUndefined();

const values = Array.from(positionSelect!.options).map((o) => o.value);
expect(values).toEqual(['top', 'bottom', 'left', 'right']);
});

it('renders Select Element button', () => {
initBarWithMessage([], 'tooltip');
const bar = document.getElementById('gist-preview-bar')!;
const selectBtn = bar.querySelector('.gist-pb-select-elem-btn');
expect(selectBtn).not.toBeNull();
expect(selectBtn!.textContent).toBe('Select Element');
});

it('renders element selector input with placeholder text', () => {
initBarWithMessage([], 'tooltip');
const bar = document.getElementById('gist-preview-bar')!;
const input = bar.querySelector<HTMLInputElement>('.gist-pb-input[type="text"]');
expect(input).not.toBeNull();
expect(input!.placeholder).toBe('Element ID or selector');
});
});

describe('inline controls', () => {
it('renders element selector for inline display type', () => {
initBarWithMessage([], 'inline');
const bar = document.getElementById('gist-preview-bar')!;
const labels = Array.from(bar.querySelectorAll('.gist-pb-label')).map((el) => el.textContent);

expect(labels).toContain('Element Selector');
});

it('does not render position dropdown for inline type', () => {
initBarWithMessage([], 'inline');
const bar = document.getElementById('gist-preview-bar')!;
const labels = Array.from(bar.querySelectorAll('.gist-pb-label')).map((el) => el.textContent);

expect(labels).not.toContain('Position');
});
});

describe('button type attributes', () => {
it('toggle button has type="button"', () => {
initBarWithMessage();
const bar = document.getElementById('gist-preview-bar')!;
const toggleBtn = bar.querySelector<HTMLButtonElement>('.gist-pb-toggle-btn');
expect(toggleBtn).not.toBeNull();
expect(toggleBtn!.type).toBe('button');
});

it('end session button has type="button"', () => {
initBarWithMessage();
const bar = document.getElementById('gist-preview-bar')!;
const endBtn = bar.querySelector<HTMLButtonElement>('.gist-pb-save-btn');
expect(endBtn).not.toBeNull();
expect(endBtn!.type).toBe('button');
});

it('select element button has type="button"', () => {
initBarWithMessage([], 'tooltip');
const bar = document.getElementById('gist-preview-bar')!;
const selectBtn = bar.querySelector<HTMLButtonElement>('.gist-pb-select-elem-btn');
expect(selectBtn).not.toBeNull();
expect(selectBtn!.type).toBe('button');
});
});

describe('updatePreviewBarStep', () => {
it('updates the bar when step changes', () => {
const steps: StepDisplayConfig[] = [
{ stepName: 'step-1', displaySettings: { displayType: 'modal' } },
{
stepName: 'step-2',
displaySettings: { displayType: 'tooltip', tooltipPosition: 'bottom' },
},
];
initBarWithMessage(steps);

updatePreviewBarStep('step-2', { displayType: 'tooltip', tooltipPosition: 'bottom' });

const bar = document.getElementById('gist-preview-bar')!;
const labels = Array.from(bar.querySelectorAll('.gist-pb-label')).map((el) => el.textContent);
expect(labels).toContain('Element Selector');
expect(labels).toContain('Position');
});
});

describe('destroyPreviewBar', () => {
it('removes the bar and styles from the DOM', () => {
initPreviewBar();
expect(document.getElementById('gist-preview-bar')).not.toBeNull();
expect(document.getElementById('gist-pb-styles')).not.toBeNull();

destroyPreviewBar();

expect(document.getElementById('gist-preview-bar')).toBeNull();
expect(document.getElementById('gist-pb-styles')).toBeNull();
});
});
});
Loading
Loading