altair-graphql-core
Version:
Several of the core logic for altair graphql client
447 lines • 20.4 kB
JavaScript
import { describe, expect, it, beforeEach, vi } from 'vitest';
import { ScriptEvaluatorWorkerEngine } from './evaluator-worker-engine';
import { SCRIPT_INIT_EXECUTE } from './events';
import { ScriptEvaluatorWorker, RequestType, } from './types';
// ---------------------------------------------------------------------------
// Mock worker
// ---------------------------------------------------------------------------
class MockWorker extends ScriptEvaluatorWorker {
constructor() {
super(...arguments);
this._messageHandlers = [];
this._errorHandlers = [];
/** Every call to send() is recorded here */
this.sentMessages = [];
/** Override the auto-reply value for a specific message type */
this.responseMap = {};
/** Message types that will NOT receive an automatic _response reply */
this.blockedTypes = new Set();
}
onMessage(handler) {
this._messageHandlers.push(handler);
}
send(type, payload) {
this.sentMessages.push({ type, payload });
// Automatically reply with a _response message unless the type is blocked
if (!type.endsWith('_response') &&
!type.endsWith('_error') &&
!this.blockedTypes.has(type)) {
const id = payload.id;
const response = Object.prototype.hasOwnProperty.call(this.responseMap, type)
? this.responseMap[type]
: undefined;
this._dispatch(`${type}_response`, { id, response });
}
}
onError(handler) {
this._errorHandlers.push(handler);
}
/** Deliver a message to all registered onMessage handlers and flush async work */
async simulateMessage(type, payload) {
this._dispatch(type, payload);
await new Promise((r) => setTimeout(r, 0));
}
/** Invoke all registered onError handlers */
simulateError(err) {
this._errorHandlers.forEach((h) => h(err));
}
getSentMessage(type) {
return this.sentMessages.find((m) => m.type === type);
}
getAllSentMessages(type) {
return this.sentMessages.filter((m) => m.type === type);
}
_dispatch(type, payload) {
[...this._messageHandlers].forEach((h) => h({ type, payload }));
}
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const createData = (overrides = {}) => ({
headers: [],
variables: '{}',
operationName: 'TestOp',
query: 'query { test }',
url: 'https://example.com/graphql',
environment: {},
requestScriptLogs: [],
...overrides,
});
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe('ScriptEvaluatorWorkerEngine', () => {
let worker;
let engine;
beforeEach(() => {
worker = new MockWorker();
engine = new ScriptEvaluatorWorkerEngine(worker);
});
describe('start()', () => {
it('registers exactly one onMessage handler on the worker', () => {
const spy = vi.spyOn(worker, 'onMessage');
engine.start();
expect(spy).toHaveBeenCalledOnce();
});
it('can be called multiple times without throwing', () => {
expect(() => {
engine.start();
engine.start();
}).not.toThrow();
});
});
describe('message routing', () => {
beforeEach(() => engine.start());
it('does nothing for unknown message types', async () => {
await worker.simulateMessage('unknown_event', { some: 'data' });
expect(worker.getSentMessage('executeComplete')).toBeUndefined();
expect(worker.getSentMessage('scriptError')).toBeUndefined();
});
it('triggers script execution for SCRIPT_INIT_EXECUTE messages', async () => {
await worker.simulateMessage(SCRIPT_INIT_EXECUTE, ['', createData()]);
expect(worker.getSentMessage('executeComplete')).toBeDefined();
});
});
describe('successful script execution', () => {
beforeEach(() => engine.start());
it('sends executeComplete after running an empty script', async () => {
await worker.simulateMessage(SCRIPT_INIT_EXECUTE, ['', createData()]);
expect(worker.getSentMessage('executeComplete')).toBeDefined();
});
it('sends executeComplete with a non-empty id', async () => {
await worker.simulateMessage(SCRIPT_INIT_EXECUTE, ['', createData()]);
const { id } = worker.getSentMessage('executeComplete')
.payload;
expect(typeof id).toBe('string');
expect(id.length).toBeGreaterThan(0);
});
it('reflects mutations the script makes to altair.data in executeComplete', async () => {
const data = createData({ query: 'original', operationName: 'GetUsers' });
await worker.simulateMessage(SCRIPT_INIT_EXECUTE, [
'altair.data.query = "updated query";',
data,
]);
const { args } = worker.getSentMessage('executeComplete')
.payload;
expect(args[0].query).toBe('updated query');
// unmutated properties are preserved
expect(args[0].operationName).toBe('GetUsers');
// original data is not mutated
expect(data.query).toBe('original');
});
});
describe('script error handling', () => {
beforeEach(() => engine.start());
it('sends scriptError when the script throws an Error', async () => {
await worker.simulateMessage(SCRIPT_INIT_EXECUTE, [
'throw new Error("oops");',
createData(),
]);
expect(worker.getSentMessage('scriptError')).toBeDefined();
});
it('includes the thrown error instance in the scriptError args', async () => {
await worker.simulateMessage(SCRIPT_INIT_EXECUTE, [
'throw new Error("test error message");',
createData(),
]);
const { args } = worker.getSentMessage('scriptError')
.payload;
expect(args[0]).toBeInstanceOf(Error);
expect(args[0].message).toBe('test error message');
});
it('does not send executeComplete when the script throws', async () => {
await worker.simulateMessage(SCRIPT_INIT_EXECUTE, [
'throw new Error("fail");',
createData(),
]);
expect(worker.getSentMessage('executeComplete')).toBeUndefined();
});
});
describe('active environment update', () => {
beforeEach(() => engine.start());
it('passes all environment updated with activeEnvironment=true to updateActiveEnvironment', async () => {
const data = createData({ environment: {} });
await worker.simulateMessage(SCRIPT_INIT_EXECUTE, [
`
altair.helpers.setEnvironment("token", "abc123", true);
altair.helpers.setEnvironment("a", 1);
altair.helpers.setEnvironment("b", 2, true);
`,
data,
]);
const { args } = worker.getSentMessage('updateActiveEnvironment').payload;
expect(args[0]).toEqual({ token: 'abc123', b: 2 });
const executeCompleteArgs = worker.getSentMessage('executeComplete')
.payload;
// The environment in executeComplete should also reflect the updates
expect(executeCompleteArgs.args[0].environment).toEqual({
token: 'abc123',
a: 1,
b: 2,
});
});
it('does not send updateActiveEnvironment when setEnvironment is called without the flag', async () => {
await worker.simulateMessage(SCRIPT_INIT_EXECUTE, [
'altair.helpers.setEnvironment("key", "value");',
createData(),
]);
expect(worker.getSentMessage('updateActiveEnvironment')).toBeUndefined();
});
it('sends updateActiveEnvironment before executeComplete', async () => {
await worker.simulateMessage(SCRIPT_INIT_EXECUTE, [
'altair.helpers.setEnvironment("x", "y", true);',
createData({ environment: {} }),
]);
const order = worker.sentMessages
.filter((m) => m.type === 'updateActiveEnvironment' || m.type === 'executeComplete')
.map((m) => m.type);
expect(order.indexOf('updateActiveEnvironment')).toBeLessThan(order.indexOf('executeComplete'));
});
});
describe('worker event handler forwarding (makeCall)', () => {
beforeEach(() => engine.start());
it('sends a setCookie message when the script calls helpers.setCookie', async () => {
await worker.simulateMessage(SCRIPT_INIT_EXECUTE, [
'altair.helpers.setCookie("sid", "val", { path: "/", secure: true });',
createData(),
]);
const { args } = worker.getSentMessage('setCookie').payload;
expect(args[2]).toEqual({ path: '/', secure: true });
});
it('sends a request message when the script calls await helpers.request', async () => {
await worker.simulateMessage(SCRIPT_INIT_EXECUTE, [
'await altair.helpers.request("GET", "https://api.example.com", null);',
createData(),
]);
const msg = worker.getSentMessage('request');
expect(msg).toBeDefined();
const { args } = msg.payload;
expect(args[0]).toBe('GET');
expect(args[1]).toBe('https://api.example.com');
expect(args[2]).toBeNull();
});
it('sends a getStorageItem message when the script calls await altair.storage.get', async () => {
await worker.simulateMessage(SCRIPT_INIT_EXECUTE, [
'await altair.storage.get("myKey");',
createData(),
]);
const msg = worker.getSentMessage('getStorageItem');
expect(msg).toBeDefined();
const { args } = msg.payload;
expect(args[0]).toBe('myKey');
});
it('sends a setStorageItem message when the script calls await altair.storage.set', async () => {
await worker.simulateMessage(SCRIPT_INIT_EXECUTE, [
'await altair.storage.set("myKey", { value: 42 });',
createData(),
]);
const msg = worker.getSentMessage('setStorageItem');
expect(msg).toBeDefined();
const { args } = msg.payload;
expect(args[0]).toBe('myKey');
expect(args[1]).toEqual({ value: 42 });
});
it('sends an alert message when the script calls alert()', async () => {
await worker.simulateMessage(SCRIPT_INIT_EXECUTE, [
'alert("hello world");',
createData(),
]);
const msg = worker.getSentMessage('alert');
expect(msg).toBeDefined();
const { args } = msg.payload;
expect(args[0]).toBe('hello world');
});
it('includes a unique id in every makeCall payload', async () => {
await worker.simulateMessage(SCRIPT_INIT_EXECUTE, [
'await altair.storage.get("k1"); await altair.storage.get("k2");',
createData(),
]);
const msgs = worker.getAllSentMessages('getStorageItem');
expect(msgs).toHaveLength(2);
const ids = msgs.map((m) => m.payload.id);
expect(ids[0]).not.toBe(ids[1]);
});
});
describe('handler response propagation', () => {
beforeEach(() => engine.start());
it('makes the request handler response available in the script', async () => {
worker.responseMap['request'] = { status: 200, body: '{"ok":true}' };
await worker.simulateMessage(SCRIPT_INIT_EXECUTE, [
`
const res = await altair.helpers.request('GET', 'https://api.example.com', null);
altair.data.variables = JSON.stringify(res);
`,
createData(),
]);
const { args } = worker.getSentMessage('executeComplete')
.payload;
expect(JSON.parse(args[0].variables)).toEqual({
status: 200,
body: '{"ok":true}',
});
});
it('makes the getStorageItem handler response available in the script', async () => {
worker.responseMap['getStorageItem'] = 'stored-value';
await worker.simulateMessage(SCRIPT_INIT_EXECUTE, [
`
const val = await altair.storage.get('someKey');
altair.data.variables = JSON.stringify(val);
`,
createData(),
]);
const { args } = worker.getSentMessage('executeComplete')
.payload;
expect(JSON.parse(args[0].variables)).toBe('stored-value');
});
});
describe('script context features', () => {
beforeEach(() => engine.start());
it('exposes environment variables via altair.helpers.getEnvironment', async () => {
const data = createData({ environment: { apiKey: 'secret-123' } });
await worker.simulateMessage(SCRIPT_INIT_EXECUTE, [
'altair.data.query = altair.helpers.getEnvironment("apiKey");',
data,
]);
const { args } = worker.getSentMessage('executeComplete')
.payload;
expect(args[0].query).toBe('secret-123');
});
it('exposes nested environment variables using dot notation', async () => {
const data = createData({ environment: { user: { name: 'Alice' } } });
await worker.simulateMessage(SCRIPT_INIT_EXECUTE, [
'altair.data.query = altair.helpers.getEnvironment("user.name");',
data,
]);
const { args } = worker.getSentMessage('executeComplete')
.payload;
expect(args[0].query).toBe('Alice');
});
it('exposes cookie values via altair.helpers.getCookie', async () => {
const data = createData({ __cookieJar: { sessionId: 'abc-xyz' } });
await worker.simulateMessage(SCRIPT_INIT_EXECUTE, [
'altair.data.query = altair.helpers.getCookie("sessionId");',
data,
]);
const { args } = worker.getSentMessage('executeComplete')
.payload;
expect(args[0].query).toBe('abc-xyz');
});
it('appends a log entry to requestScriptLogs when altair.log is called', async () => {
await worker.simulateMessage(SCRIPT_INIT_EXECUTE, [
'altair.log("hello from script");',
createData(),
]);
const { args } = worker.getSentMessage('executeComplete')
.payload;
expect(args[0].requestScriptLogs).toHaveLength(1);
expect(args[0].requestScriptLogs?.[0]?.text).toBe('"hello from script"');
});
it('serializes complex log values as JSON', async () => {
await worker.simulateMessage(SCRIPT_INIT_EXECUTE, [
'altair.log({ count: 3, items: ["a", "b"] });',
createData(),
]);
const { args } = worker.getSentMessage('executeComplete')
.payload;
expect(JSON.parse(args[0].requestScriptLogs[0].text)).toEqual({
count: 3,
items: ['a', 'b'],
});
});
it('exposes altair.response when response data is present in the context', async () => {
const data = createData({
requestType: RequestType.QUERY,
response: {
ok: true,
body: '{"data":{"users":[]}}',
headers: { 'content-type': 'application/json' },
status: 200,
statusText: 'OK',
url: 'https://example.com/graphql',
requestStartTime: 0,
requestEndTime: 100,
responseTime: 100,
},
});
await worker.simulateMessage(SCRIPT_INIT_EXECUTE, [
'altair.data.query = altair.response ? "has-response" : "no-response";',
data,
]);
const { args } = worker.getSentMessage('executeComplete')
.payload;
expect(args[0].query).toBe('has-response');
});
it('exposes the correct status code via altair.response.statusCode', async () => {
const data = createData({
requestType: RequestType.QUERY,
response: {
ok: true,
body: '',
headers: {},
status: 201,
statusText: 'Created',
url: 'https://example.com/graphql',
requestStartTime: 0,
requestEndTime: 50,
responseTime: 50,
},
});
await worker.simulateMessage(SCRIPT_INIT_EXECUTE, [
'altair.data.query = String(altair.response.statusCode);',
data,
]);
const { args } = worker.getSentMessage('executeComplete')
.payload;
expect(args[0].query).toBe('201');
});
it('altair.response is undefined when no response data is provided', async () => {
await worker.simulateMessage(SCRIPT_INIT_EXECUTE, [
'altair.data.query = altair.response ? "has-response" : "no-response";',
createData(),
]);
const { args } = worker.getSentMessage('executeComplete')
.payload;
expect(args[0].query).toBe('no-response');
});
});
describe('worker error handling', () => {
beforeEach(() => engine.start());
it('sends scriptError when the worker emits an error during execution', async () => {
// Block request auto-reply so the script suspends at the await
worker.blockedTypes.add('request');
await worker.simulateMessage(SCRIPT_INIT_EXECUTE, [
'await altair.helpers.request("GET", "https://api.example.com", null);',
createData(),
]);
// The error handler inside initExecute is registered synchronously
// before the first await, so we can fire it immediately after the flush
worker.simulateError(new Error('worker crashed'));
await new Promise((r) => setTimeout(r, 0));
expect(worker.getSentMessage('scriptError')).toBeDefined();
});
it('includes the worker error in the scriptError args', async () => {
worker.blockedTypes.add('request');
await worker.simulateMessage(SCRIPT_INIT_EXECUTE, [
'await altair.helpers.request("GET", "https://api.example.com", null);',
createData(),
]);
worker.simulateError(new Error('connection lost'));
await new Promise((r) => setTimeout(r, 0));
const { args } = worker.getSentMessage('scriptError')
.payload;
expect(args[0].message).toBe('connection lost');
});
it('does not send executeComplete when a worker error occurs', async () => {
worker.blockedTypes.add('request');
await worker.simulateMessage(SCRIPT_INIT_EXECUTE, [
'await altair.helpers.request("GET", "https://api.example.com", null);',
createData(),
]);
worker.simulateError(new Error('crashed'));
await new Promise((r) => setTimeout(r, 0));
expect(worker.getSentMessage('executeComplete')).toBeUndefined();
});
});
});
//# sourceMappingURL=evaluator-worker-engine.spec.js.map