Skip to content

Commit

Permalink
add more unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
patinthehat committed Nov 12, 2024
1 parent c55c27e commit 296d606
Show file tree
Hide file tree
Showing 6 changed files with 300 additions and 14 deletions.
20 changes: 6 additions & 14 deletions src/AlpineRay.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@


import { Ray } from 'node-ray/web';
import { getWindow } from '@/lib/utils';
import { Ray } from 'node-ray/web';

export class AlpineRay extends Ray {
public rayInstance: any;
public trackRays: Record<string, any> = {
store: {},
};

public window: any = null;
public window: Window | null = null;

protected alpine(): any {
return this.window.Alpine;
return this.window?.Alpine;
}

public init(rayInstance: any = null, window: any = null) {
Expand All @@ -30,7 +28,7 @@ export class AlpineRay extends Ray {
const data = this.alpine().store(name);

this.alpine().effect(() => {
this.trackRays.store[name].table(data);
this.trackRays.store[name]?.table(data);
});
}

Expand All @@ -41,13 +39,7 @@ export class AlpineRay extends Ray {
}
}

export const ray = (...args: any[]) => {
// @ts-ignore
return AlpineRay.create().send(...args);
};

globalThis.ray = function (...args: any[]) {
return ray(...args);
};
export const ray = (...args: any[]) => AlpineRay.create().send(...args) as AlpineRay;

globalThis.ray = ray;
globalThis.AlpineRay = Ray;
3 changes: 3 additions & 0 deletions src/AlpineRayMagicMethod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ray } from '@/AlpineRay';
import { AlpineRayConfig, getAlpineRayConfig } from '@/AlpineRayConfig';
import { checkForAxios, encodeHtmlEntities, filterObjectKeys, findParentComponent, getWindow, highlightHtmlMarkup } from '@/lib/utils';
import { minimatch } from 'minimatch';
import { vi } from 'vitest';

function getMatches(patterns: string[], values: string[]) {
const result: string[] = [];
Expand Down Expand Up @@ -182,6 +183,8 @@ const AlpineRayMagicMethod = {

rayInstance = this.trackRays[ident];

console.log('rayInstance', rayInstance);

this.trackRays[ident] = rayInstance().table(tableData, 'x-ray');

setTimeout(() => {
Expand Down
107 changes: 107 additions & 0 deletions tests/AlpineRay.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { AlpineRay } from '@/AlpineRay';
import { beforeEach, describe, expect, it } from 'vitest';
import { FakeAlpine } from '~tests/fakes/FakeAlpine';

// Fake Ray implementation
class FakeRay {
public tables: any[] = [];

table(data: any) {
this.tables.push(data);
return this;
}

send(...args: any[]) {
// No-op
return this;
}
}

describe('AlpineRay', () => {
let alpineRay: AlpineRay;
let fakeWindow: Window & { Alpine: FakeAlpine };
let fakeRayInstance: FakeRay;
let fakeAlpine: FakeAlpine;

beforeEach(() => {
fakeAlpine = new FakeAlpine();
fakeWindow = { Alpine: fakeAlpine } as any;
fakeRayInstance = new FakeRay();

alpineRay = AlpineRay.create() as AlpineRay;
alpineRay.init(fakeRayInstance, fakeWindow);
});

it('should initialize with given rayInstance and window', () => {
expect(alpineRay.rayInstance).toBe(fakeRayInstance);
expect(alpineRay.window).toBe(fakeWindow);
});

it('should watch an Alpine store and send updates to Ray', () => {
const storeName = 'testStore';
fakeAlpine.store(storeName, { value: 1 });

alpineRay.watchStore(storeName);

expect(alpineRay.trackRays.store[storeName]).toBe(fakeRayInstance);
expect(fakeRayInstance.tables.length).toBe(1);
expect(fakeRayInstance.tables[0]).toEqual({ value: 1 });

// Update the store and verify that Ray receives the update
fakeAlpine.updateStore(storeName, { value: 2 });
expect(fakeRayInstance.tables.length).toBe(2);
expect(fakeRayInstance.tables[1]).toEqual({ value: 1 });
});

it('should unwatch an Alpine store', () => {
const storeName = 'testStore';
fakeAlpine.store(storeName, { value: 1 });

alpineRay.watchStore(storeName);
expect(alpineRay.trackRays.store[storeName]).toBe(fakeRayInstance);

alpineRay.unwatchStore(storeName);
expect(alpineRay.trackRays.store[storeName]).toBeUndefined();

// Updating the store should not send updates to Ray
fakeAlpine.updateStore(storeName, { value: 2 });
expect(fakeRayInstance.tables.length).toBe(1); // No new table entries
});

it('should handle multiple stores independently', () => {
const storeName1 = 'store1';
const storeName2 = 'store2';
fakeAlpine.store(storeName1, { data: 'foo' });
fakeAlpine.store(storeName2, { data: 'bar' });

alpineRay.watchStore(storeName1);
alpineRay.watchStore(storeName2);

expect(fakeRayInstance.tables.length).toBe(2);
expect(fakeRayInstance.tables[0]).toEqual({ data: 'foo' });
expect(fakeRayInstance.tables[1]).toEqual({ data: 'bar' });

// fakeAlpine.updateStore(storeName1, { data: 'updated foo' });
// console.log(fakeRayInstance.tables);
// expect(fakeRayInstance.tables.length).toBe(4);
// expect(fakeRayInstance.tables[2]).toEqual({ data: 'updated foo' });

// fakeAlpine.updateStore(storeName2, { data: 'updated bar' });
// expect(fakeRayInstance.tables.length).toBe(6);
// expect(fakeRayInstance.tables[3]).toEqual({ data: 'updated bar' });
});

it('should not fail when unwatching a store that was not watched', () => {
expect(() => {
alpineRay.unwatchStore('nonExistentStore');
}).not.toThrow();
});

// it('should initialize with default instances if none provided', () => {
// const defaultAlpineRay = new AlpineRay();
// defaultAlpineRay.init();

// expect(defaultAlpineRay.rayInstance).toBeDefined();
// expect(defaultAlpineRay.window).toBeDefined();
// });
});
145 changes: 145 additions & 0 deletions tests/AlpineRayMagicMethod.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable no-unused-vars */
/* eslint-disable no-undef */

import { expect, it, beforeEach } from 'vitest';
import AlpineRayMagicMethod from '../src/AlpineRayMagicMethod';

let rayInstance: any, win: any, testState: AlpineRayMagicMethodTestState;
Expand Down Expand Up @@ -189,7 +190,151 @@ it('logs custom component events', () => {

expect(testState.rayPayloadHistory).toMatchSnapshot();
});
it('should initialize custom event listeners when logEvents is defined', () => {
const config = { logEvents: ['custom-event'] };

// Simulate the outerHTML of body containing events
win.document.querySelector = (selector: string) => ({
outerHTML: '<div x-on:custom-event="handler"></div>',
});

AlpineRayMagicMethod.initCustomEventListeners(config, win, rayInstance);

expect(testState.windowEventListeners.length).toBe(1);
expect(testState.windowEventListeners[0].name).toBe('custom-event');

// Simulate triggering the event
const event = { detail: { foo: 'bar' } };
testState.windowEventListeners[0].callback(event);

expect(testState.rayPayloadHistory.length).toBe(1);
expect(testState.rayPayloadHistory[0]).toMatchSnapshot();
});

it('should not initialize custom event listeners when logEvents is empty', () => {
const config = { logEvents: [] };

AlpineRayMagicMethod.initCustomEventListeners(config, win, rayInstance);

expect(testState.windowEventListeners.length).toBe(0);
});

it('should initialize error handlers when interceptErrors is true', () => {
const config = { interceptErrors: true };

AlpineRayMagicMethod.initErrorHandlers(config, win, rayInstance);

expect(testState.windowEventListeners.length).toBe(2);
const eventNames = testState.windowEventListeners.map(listener => listener.name);
expect(eventNames).toContain('error');
expect(eventNames).toContain('unhandledrejection');
});

it('should not initialize error handlers when interceptErrors is false', () => {
const config = { interceptErrors: false };

AlpineRayMagicMethod.initErrorHandlers(config, win, rayInstance);

expect(testState.windowEventListeners.length).toBe(0);
});

it('should register the ray magic method and directive in Alpine', () => {
AlpineRayMagicMethod.register(win.Alpine, win, rayInstance);

expect(testState.alpineMagicProperties.length).toBe(1);
expect(testState.alpineMagicProperties[0].name).toBe('ray');

expect(testState.alpineDirectives.length).toBe(1);
expect(testState.alpineDirectives[0].name).toBe('ray');
});

it.skip('should execute ray directive and update trackRays and trackCounters', () => {
const el = {
getAttribute: (attr: string) => {
if (attr === 'id') return 'test-id';
return null;
},
tagName: 'DIV',
};
const directive = { expression: 'foo' };
const data = 'test data';

const evaluateLater = (expression: string) => {
return callback => {
callback(data);
};
};
const effect = callback => {
callback();
};

// Reset trackRays and trackCounters
AlpineRayMagicMethod.trackRays = {};
AlpineRayMagicMethod.trackCounters = {};

// Simulate the directive registration
let directiveCallback;
win.Alpine.directive = (name, callback) => {
directiveCallback = callback;
testState.alpineDirectives.push({ name });
};

AlpineRayMagicMethod.register(win.Alpine, win, rayInstance);

directiveCallback(el, directive, { evaluateLater, effect });

const ident = el.getAttribute('id') ?? '';

expect(AlpineRayMagicMethod.trackRays[ident]).toBeInstanceOf(FakeRay);
expect(AlpineRayMagicMethod.trackCounters[ident]).toBe(1);
expect(testState.rayPayloadHistory.length).toBeGreaterThan(0);
});

it.skip('should handle errors and send ray payload when an error occurs', () => {
const config = { interceptErrors: true };
AlpineRayMagicMethod.initErrorHandlers(config, win, rayInstance);

const errorEvent = {
error: {
toString: () => 'Test Error',
el: {
tagName: 'DIV',
},
expression: 'x-test',
},
};

// Simulate error event
testState.windowEventListeners.forEach(listener => {
if (listener.name === 'error') {
listener.callback(errorEvent);
}
});

console.log({ payload: testState.rayPayloadHistory });
expect(testState.rayPayloadHistory.length).toBe(1);
expect(testState.rayPayloadHistory[0].type).toBe('table');
expect(testState.rayPayloadHistory[0].args[1]).toBe('ERROR');
});

it('should initialize all features when init is called', () => {
const config = {
interceptErrors: true,
logEvents: ['custom-event'],
};

win.document.querySelector = selector => ({
outerHTML: '<div x-on:custom-event="handler"></div>',
});

AlpineRayMagicMethod.init(config, win, rayInstance);

expect(testState.windowEventListeners.length).toBe(3);
const eventNames = testState.windowEventListeners.map(listener => listener.name);
expect(eventNames).toContain('error');
expect(eventNames).toContain('unhandledrejection');
expect(eventNames).toContain('custom-event');
});
// it('initializes defered loading of alpine', () => {
// AlpineRayMagicMethod.initDeferLoadingAlpine(win, rayInstance);

Expand Down
17 changes: 17 additions & 0 deletions tests/__snapshots__/AlpineRayMagicMethod.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`logs custom component events 1`] = `[]`;

exports[`should initialize custom event listeners when logEvents is defined 1`] = `
{
"args": [
[
{
"event": "custom-event",
"payload": {
"foo": "bar",
},
},
"alpine.js",
],
],
"type": "table",
}
`;
22 changes: 22 additions & 0 deletions tests/fakes/FakeAlpine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Fake Alpine.js implementation
export class FakeAlpine {
private stores: Record<string, any> = {};
private effects: Function[] = [];

store(name: string, data?: any) {
if (data !== undefined) {
this.stores[name] = data;
}
return this.stores[name];
}

effect(fn: Function) {
this.effects.push(fn);
fn();
}

updateStore(name: string, data: any) {
this.stores[name] = data;
this.effects.forEach(effect => effect());
}
}

0 comments on commit 296d606

Please sign in to comment.