Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions packages/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"@types/express": "^5.0.6",
"@types/react": "^18.3.3",
"@types/supertest": "^7.2.1",
"jsdom": "^30.1.1",
"openapi-typescript": "^7.13.0",
"supertest": "^7.2.2",
"tsup": "^8.0.0",
Expand Down
45 changes: 45 additions & 0 deletions packages/sdk/src/client.stream.dom.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/** @vitest-environment jsdom */
import { afterEach, describe, expect, it, vi } from 'vitest';
import { AccensaClient } from './client';
import type { SyncEvent } from './types/sync-event';

class MockEventSource extends EventTarget {
static instances: MockEventSource[] = [];
readonly close = vi.fn();

constructor(readonly url: string) {
super();
MockEventSource.instances.push(this);
}
}

describe('AccensaClient sync event stream', () => {
afterEach(() => {
vi.unstubAllGlobals();
MockEventSource.instances = [];
});

it('delivers sync events and closes the stream when unsubscribed', () => {
vi.stubGlobal('EventSource', MockEventSource);
const client = new AccensaClient({ indexerUrl: 'https://indexer.example' });
const onSync = vi.fn();
const unsubscribe = client.subscribeSync({ onSync });
const source = MockEventSource.instances[0];
const payload: SyncEvent = {
merchant: 'G' + 'A'.repeat(55),
syncedTo: 42,
inserted: 2,
scanned: 3,
pages: 1,
drained: true,
occurredAt: '2026-10-17T00:00:00.000Z',
};

source.dispatchEvent(new MessageEvent('sync', { data: JSON.stringify(payload) }));

expect(source.url).toBe('https://indexer.example/api/sync/stream');
expect(onSync).toHaveBeenCalledWith(payload);
unsubscribe();
expect(source.close).toHaveBeenCalledOnce();
});
});
28 changes: 28 additions & 0 deletions packages/sdk/src/offline-sync.dom.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/** @vitest-environment jsdom */
import { afterEach, describe, expect, it } from 'vitest';
import { OfflineSyncEngine } from './offline-sync';

describe('OfflineSyncEngine in a browser DOM', () => {
afterEach(() => {
localStorage.clear();
window.dispatchEvent(new Event('online'));
});

it('persists queued work offline and flushes it when connectivity returns', async () => {
Object.defineProperty(navigator, 'onLine', { configurable: true, value: true });
const engine = new OfflineSyncEngine({ storageKey: 'offline-dom-test', syncIntervalMs: 0 });
await engine.init();

window.dispatchEvent(new Event('offline'));
engine.queueUpdate('orders', 'order-1', { status: 'ready' });

expect(engine.getPendingCount()).toBe(1);
expect(JSON.parse(localStorage.getItem('offline-dom-test')!).pendingOps).toHaveLength(1);

window.dispatchEvent(new Event('online'));
await Promise.resolve();

expect(engine.getPendingCount()).toBe(0);
expect(JSON.parse(localStorage.getItem('offline-dom-test')!).pendingOps).toHaveLength(0);
});
});
28 changes: 28 additions & 0 deletions packages/sdk/src/widget/checkout-widget.dom.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/** @vitest-environment jsdom */
import { afterEach, describe, expect, it, vi } from 'vitest';
import { AccensaCheckoutWidget } from './checkout-widget';

describe('AccensaCheckoutWidget DOM lifecycle', () => {
afterEach(() => {
vi.restoreAllMocks();
document.body.replaceChildren();
});

it('renders when connected and removes its message listener on disconnect', () => {
expect(customElements.get('accensa-checkout')).toBe(AccensaCheckoutWidget);
const add = vi.spyOn(window, 'addEventListener');
const remove = vi.spyOn(window, 'removeEventListener');
const widget = document.createElement('accensa-checkout') as AccensaCheckoutWidget;
widget.setAttribute('amount', '12.50');
document.body.append(widget);

expect(widget.shadowRoot?.textContent).toContain('12.50');
const registered = add.mock.calls.find(([type]) => type === 'message')?.[1];
expect(registered).toBeTypeOf('function');

widget.remove();

const removed = remove.mock.calls.find(([type]) => type === 'message')?.[1];
expect(removed).toBe(registered);
});
});
7 changes: 5 additions & 2 deletions packages/sdk/src/widget/checkout-widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ class AccensaCheckoutWidget extends HTMLElementBase {
}

disconnectedCallback() {
window.removeEventListener('message', this.handleMessage);
window.removeEventListener('message', this.boundHandleMessage);
}

attributeChangedCallback(name: string, oldValue: string, newValue: string) {
Expand All @@ -88,7 +88,7 @@ class AccensaCheckoutWidget extends HTMLElementBase {
}

private setupMessageListener() {
window.addEventListener('message', this.handleMessage.bind(this));
window.addEventListener('message', this.boundHandleMessage);
}

private handleMessage = (event: MessageEvent<ParentMessage>) => {
Expand Down Expand Up @@ -119,6 +119,9 @@ class AccensaCheckoutWidget extends HTMLElementBase {
}
};

// Retain the same bound callback so disconnection can remove the listener.
private boundHandleMessage = this.handleMessage.bind(this);

private postMessage(message: WidgetMessage, targetOrigin: string = MESSAGE_ORIGIN) {
if (this.messageId > 10000) {
this.messageId = 0; // Prevent overflow
Expand Down
2 changes: 2 additions & 0 deletions packages/sdk/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { defineConfig } from 'vitest/config';

export default defineConfig({
test: {
// DOM tests opt in with a per-file @vitest-environment jsdom docblock so
// the SDK's existing Node-only tests keep their current runtime.
/**
* `tsc` now runs alongside the suite, which starves the event loop enough
* on a loaded machine to push the timer-driven tests (the abort and retry
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading