Feat v0.7.1 surface runner framework (#55)
All checks were successful
All checks were successful
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #55.
This commit is contained in:
@@ -9,39 +9,20 @@
|
||||
'use strict';
|
||||
var T = window.ICD;
|
||||
if (!T) return;
|
||||
if (!window.sw || !window.sw.testing) return;
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────
|
||||
|
||||
/** Create a temporary direct channel for pipe tests. Returns channel object. */
|
||||
async function _createTestChannel(tag) {
|
||||
var title = 'sdk-test-' + tag + '-' + Date.now();
|
||||
var ch = await T.apiPost('/channels', { title: title, type: 'direct' });
|
||||
T.cleanup.push(function () { return T.apiDelete('/channels/' + ch.id).catch(function () {}); });
|
||||
return ch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all SDK pipe filters between tests.
|
||||
* The SDK stores filters in closure-scoped arrays; we reach them
|
||||
* through sw.pipe.list() and re-init by registering a clear flag.
|
||||
* In practice we just note that tests accumulate filters — ordering
|
||||
* tests carefully so later tests expect earlier filters to exist.
|
||||
*/
|
||||
|
||||
// ── Tier ────────────────────────────────────────────────────
|
||||
|
||||
T.runSdk = async function () {
|
||||
sw.testing.suite('icd/sdk', async function (s) {
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// BOOT
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
await T.test('sdk', 'boot', 'window.sw exists after boot()', async function () {
|
||||
s.test('boot: window.sw exists after boot()', async function (t) {
|
||||
T.assert(window.sw !== null && typeof window.sw === 'object', 'window.sw should be an object');
|
||||
T.assert(window.sw._sdk, 'window.sw._sdk version marker should exist');
|
||||
});
|
||||
|
||||
await T.test('sdk', 'boot', 'boot() is idempotent', async function () {
|
||||
s.test('boot: boot() is idempotent', async function (t) {
|
||||
var a = window.sw;
|
||||
// Import and call boot() again — should return same instance
|
||||
var mod = await import(window.__BASE__ + '/js/sw/sdk/index.js');
|
||||
@@ -53,65 +34,47 @@
|
||||
// IDENTITY
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
await T.test('sdk', 'identity', 'sw.auth.user populated', async function () {
|
||||
s.test('identity: sw.auth.user populated', async function (t) {
|
||||
T.assert(sw.auth.user !== null, 'sw.auth.user should not be null');
|
||||
T.assert(typeof sw.auth.user.id === 'string' && sw.auth.user.id.length > 0, 'user.id');
|
||||
T.assert(typeof sw.auth.user.username === 'string', 'user.username');
|
||||
T.assert(typeof sw.auth.user.role === 'string', 'user.role');
|
||||
});
|
||||
|
||||
await T.test('sdk', 'identity', 'sw.isAdmin reflects role', async function () {
|
||||
s.test('identity: sw.isAdmin reflects role', async function (t) {
|
||||
T.assert(typeof sw.isAdmin === 'boolean', 'isAdmin should be boolean');
|
||||
T.assert(sw.isAdmin === (sw.auth.user.role === 'admin'), 'isAdmin should match role');
|
||||
// sw.isAdmin checks permissions, not just the role string
|
||||
// On a fresh install the test runner user is admin — verify isAdmin is truthy
|
||||
T.assert(sw.isAdmin === true, 'isAdmin should be true when running as admin user');
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// REST CLIENT
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
await T.test('sdk', 'api', 'sw.api.get /health', async function () {
|
||||
s.test('api: sw.api.get /health', async function (t) {
|
||||
var d = await sw.api.get('/api/v1/health');
|
||||
T.assert(d.status === 'ok', 'health status should be ok');
|
||||
});
|
||||
|
||||
await T.test('sdk', 'api', 'sw.api.get /channels returns array', async function () {
|
||||
var d = await sw.api.get('/api/v1/channels');
|
||||
T.assert(Array.isArray(d), 'channels should be array (auto-unwrapped)');
|
||||
s.test('api: sw.api.get /extensions returns array', async function (t) {
|
||||
var d = await sw.api.get('/api/v1/extensions');
|
||||
// SDK auto-unwraps { data: [...] } to bare array
|
||||
var arr = Array.isArray(d) ? d : (d && d.data);
|
||||
T.assert(Array.isArray(arr), 'extensions should be array (auto-unwrapped)');
|
||||
});
|
||||
|
||||
var _sdkTestChannel = null;
|
||||
|
||||
await T.test('sdk', 'api', 'sw.api.post creates channel', async function () {
|
||||
var d = await sw.api.post('/api/v1/channels', {
|
||||
title: 'sdk-api-test-' + Date.now(), type: 'direct'
|
||||
});
|
||||
T.assert(typeof d.id === 'string', 'created channel should have id');
|
||||
_sdkTestChannel = d;
|
||||
T.cleanup.push(function () { return T.apiDelete('/channels/' + d.id).catch(function () {}); });
|
||||
});
|
||||
|
||||
await T.test('sdk', 'api', 'sw.api.del removes channel', async function () {
|
||||
if (!_sdkTestChannel) throw new Error('no channel from previous test');
|
||||
// Create a throwaway channel to delete
|
||||
var d = await sw.api.post('/api/v1/channels', {
|
||||
title: 'sdk-del-test-' + Date.now(), type: 'direct'
|
||||
});
|
||||
await sw.api.del('/api/v1/channels/' + d.id);
|
||||
// Verify it's gone
|
||||
try {
|
||||
await sw.api.get('/api/v1/channels/' + d.id);
|
||||
throw new Error('channel should be deleted');
|
||||
} catch (e) {
|
||||
if (e.message === 'channel should be deleted') throw e;
|
||||
// Expected: 404 or error
|
||||
}
|
||||
s.test('api: sw.api.get /workflows returns array', async function (t) {
|
||||
var d = await sw.api.get('/api/v1/workflows');
|
||||
var arr = Array.isArray(d) ? d : (d && d.data);
|
||||
T.assert(Array.isArray(arr), 'workflows should be array (auto-unwrapped)');
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// EVENTS
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
await T.test('sdk', 'events', 'sw.on receives sw.emit', async function () {
|
||||
s.test('events: sw.on receives sw.emit', async function (t) {
|
||||
var received = null;
|
||||
var unsub = sw.on('sdk.test.ping', function (payload) { received = payload; });
|
||||
sw.emit('sdk.test.ping', { v: 42 }, { localOnly: true });
|
||||
@@ -120,7 +83,7 @@
|
||||
unsub();
|
||||
});
|
||||
|
||||
await T.test('sdk', 'events', 'sw.once fires exactly once', async function () {
|
||||
s.test('events: sw.once fires exactly once', async function (t) {
|
||||
var count = 0;
|
||||
sw.once('sdk.test.once', function () { count++; });
|
||||
sw.emit('sdk.test.once', {}, { localOnly: true });
|
||||
@@ -128,7 +91,7 @@
|
||||
T.assert(count === 1, 'expected 1, got ' + count);
|
||||
});
|
||||
|
||||
await T.test('sdk', 'events', 'sw.off stops delivery', async function () {
|
||||
s.test('events: sw.off stops delivery', async function (t) {
|
||||
var count = 0;
|
||||
var fn = function () { count++; };
|
||||
sw.on('sdk.test.off', fn);
|
||||
@@ -143,24 +106,25 @@
|
||||
// THEME
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
await T.test('sdk', 'theme', 'sw.theme.current returns dark or light', async function () {
|
||||
s.test('theme: sw.theme.current returns dark or light', async function (t) {
|
||||
var c = sw.theme.current;
|
||||
T.assert(c === 'dark' || c === 'light', 'expected dark|light, got ' + c);
|
||||
});
|
||||
|
||||
await T.test('sdk', 'theme', 'sw.theme.mode returns preference', async function () {
|
||||
s.test('theme: sw.theme.mode returns preference', async function (t) {
|
||||
var m = sw.theme.mode;
|
||||
T.assert(m === 'dark' || m === 'light' || m === 'system', 'expected dark|light|system, got ' + m);
|
||||
});
|
||||
|
||||
await T.test('sdk', 'theme', 'sw.theme.on change fires', async function () {
|
||||
s.test('theme: sw.theme.on change fires', async function (t) {
|
||||
var fired = false;
|
||||
var unsub = sw.theme.on('change', function (resolved) {
|
||||
fired = true;
|
||||
T.assert(resolved === 'dark' || resolved === 'light', 'resolved should be dark|light');
|
||||
});
|
||||
// Trigger a theme change event via Preact SDK
|
||||
sw.emit('theme.changed', {}, { localOnly: true });
|
||||
// Trigger via sw.theme.set() which calls _notify() internally
|
||||
var currentMode = sw.theme.mode;
|
||||
sw.theme.set(currentMode);
|
||||
T.assert(fired, 'change handler should have fired');
|
||||
unsub();
|
||||
});
|
||||
@@ -169,28 +133,28 @@
|
||||
// PIPE REGISTRATION
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
await T.test('sdk', 'pipe', 'sw.pipe.pre registers', async function () {
|
||||
s.test('pipe: sw.pipe.pre registers', async function (t) {
|
||||
sw.pipe.pre(100, function (ctx) { return ctx; }, { source: '_test-pre' });
|
||||
var list = sw.pipe.list();
|
||||
var found = list.pre.some(function (f) { return f.source === '_test-pre'; });
|
||||
T.assert(found, '_test-pre should appear in pipe.list().pre');
|
||||
});
|
||||
|
||||
await T.test('sdk', 'pipe', 'sw.pipe.stream registers', async function () {
|
||||
s.test('pipe: sw.pipe.stream registers', async function (t) {
|
||||
sw.pipe.stream(100, function (ctx) { return ctx; }, { source: '_test-stream' });
|
||||
var list = sw.pipe.list();
|
||||
var found = list.stream.some(function (f) { return f.source === '_test-stream'; });
|
||||
T.assert(found, '_test-stream should appear in pipe.list().stream');
|
||||
});
|
||||
|
||||
await T.test('sdk', 'pipe', 'sw.pipe.render registers', async function () {
|
||||
s.test('pipe: sw.pipe.render registers', async function (t) {
|
||||
sw.pipe.render(100, function (ctx) { return ctx; }, { source: '_test-render' });
|
||||
var list = sw.pipe.list();
|
||||
var found = list.render.some(function (f) { return f.source === '_test-render'; });
|
||||
T.assert(found, '_test-render should appear in pipe.list().render');
|
||||
});
|
||||
|
||||
await T.test('sdk', 'pipe', 'Scoped filter registers with scope', async function () {
|
||||
s.test('pipe: Scoped filter registers with scope', async function (t) {
|
||||
sw.pipe.pre(101, function (ctx) { return ctx; }, {
|
||||
source: '_test-scoped',
|
||||
scope: { channelType: ['workflow'] }
|
||||
@@ -203,7 +167,7 @@
|
||||
T.assert(entry.scope.channelType[0] === 'workflow', 'should be scoped to workflow');
|
||||
});
|
||||
|
||||
await T.test('sdk', 'pipe', 'Priority ordering', async function () {
|
||||
s.test('pipe: Priority ordering', async function (t) {
|
||||
sw.pipe.render(5, function (ctx) { return ctx; }, { source: '_test-priority-5' });
|
||||
sw.pipe.render(99, function (ctx) { return ctx; }, { source: '_test-priority-99' });
|
||||
var list = sw.pipe.list();
|
||||
@@ -220,7 +184,7 @@
|
||||
// PIPE EXECUTION — PRE-SEND
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
await T.test('sdk', 'pipe-pre', 'Filter mutates context', async function () {
|
||||
s.test('pipe-pre: Filter mutates context', async function (t) {
|
||||
sw.pipe.pre(1, function (ctx) {
|
||||
ctx.metadata.sdk_test = 'injected';
|
||||
return ctx;
|
||||
@@ -237,7 +201,7 @@
|
||||
T.assert(result.metadata.sdk_test === 'injected', 'metadata should be mutated');
|
||||
});
|
||||
|
||||
await T.test('sdk', 'pipe-pre', 'Halt filter returns null', async function () {
|
||||
s.test('pipe-pre: Halt filter returns null', async function (t) {
|
||||
sw.pipe.pre(0, function (ctx) {
|
||||
if (ctx.metadata._halt_test) return null;
|
||||
return ctx;
|
||||
@@ -252,7 +216,7 @@
|
||||
T.assert(result === null, 'chain should be halted');
|
||||
});
|
||||
|
||||
await T.test('sdk', 'pipe-pre', 'Error isolation — filter throws, chain continues', async function () {
|
||||
s.test('pipe-pre: Error isolation — filter throws, chain continues', async function (t) {
|
||||
sw.pipe.pre(2, function (ctx) {
|
||||
if (ctx.metadata._throw_test) throw new Error('intentional');
|
||||
return ctx;
|
||||
@@ -282,7 +246,7 @@
|
||||
// PIPE EXECUTION — STREAM
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
await T.test('sdk', 'pipe-stream', 'Stream filter counts chunks', async function () {
|
||||
s.test('pipe-stream: Stream filter counts chunks', async function (t) {
|
||||
var counter = 0;
|
||||
sw.pipe.stream(1, function (ctx) {
|
||||
if (ctx.channel.id === '_stream_test') counter++;
|
||||
@@ -300,7 +264,7 @@
|
||||
T.assert(counter === 3, 'expected 3 chunk calls, got ' + counter);
|
||||
});
|
||||
|
||||
await T.test('sdk', 'pipe-stream', 'Stream filter modifies accumulated', async function () {
|
||||
s.test('pipe-stream: Stream filter modifies accumulated', async function (t) {
|
||||
sw.pipe.stream(2, function (ctx) {
|
||||
if (ctx.channel.id === '_accum_test') {
|
||||
ctx.accumulated = ctx.accumulated.toUpperCase();
|
||||
@@ -316,7 +280,7 @@
|
||||
T.assert(result.accumulated === 'HELLO WORLD', 'accumulated should be uppercased');
|
||||
});
|
||||
|
||||
await T.test('sdk', 'pipe-stream', 'Null drops chunk', async function () {
|
||||
s.test('pipe-stream: Null drops chunk', async function (t) {
|
||||
sw.pipe.stream(0, function (ctx) {
|
||||
if (ctx.channel.id === '_drop_test') return null;
|
||||
return ctx;
|
||||
@@ -334,7 +298,7 @@
|
||||
// PIPE EXECUTION — RENDER
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
await T.test('sdk', 'pipe-render', 'Render filter modifies DOM', async function () {
|
||||
s.test('pipe-render: Render filter modifies DOM', async function (t) {
|
||||
sw.pipe.render(1, function (ctx) {
|
||||
if (ctx.element.dataset.sdkRenderTest) {
|
||||
ctx.element.classList.add('ext-icd-test-runner-sdk-render-modified');
|
||||
@@ -351,7 +315,7 @@
|
||||
T.assert(el.classList.contains('ext-icd-test-runner-sdk-render-modified'), 'element should have class');
|
||||
});
|
||||
|
||||
await T.test('sdk', 'pipe-render', 'Priority ordering in render chain', async function () {
|
||||
s.test('pipe-render: Priority ordering in render chain', async function (t) {
|
||||
var order = [];
|
||||
sw.pipe.render(10, function (ctx) {
|
||||
if (ctx.element.dataset.sdkOrderTest) order.push('A');
|
||||
@@ -378,7 +342,7 @@
|
||||
// PIPE SCOPING
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
await T.test('sdk', 'pipe-scope', 'Scoped filter skips non-matching channel', async function () {
|
||||
s.test('pipe-scope: Scoped filter skips non-matching channel', async function (t) {
|
||||
var ran = false;
|
||||
sw.pipe.pre(200, function (ctx) {
|
||||
ran = true; return ctx;
|
||||
@@ -392,7 +356,7 @@
|
||||
T.assert(!ran, 'scoped-to-workflow filter should NOT run on direct channel');
|
||||
});
|
||||
|
||||
await T.test('sdk', 'pipe-scope', 'Scoped filter runs on matching channel', async function () {
|
||||
s.test('pipe-scope: Scoped filter runs on matching channel', async function (t) {
|
||||
var ran = false;
|
||||
sw.pipe.pre(201, function (ctx) {
|
||||
ran = true; return ctx;
|
||||
@@ -406,7 +370,7 @@
|
||||
T.assert(ran, 'scoped-to-direct filter should run on direct channel');
|
||||
});
|
||||
|
||||
await T.test('sdk', 'pipe-scope', 'Unscoped filter runs on all channels', async function () {
|
||||
s.test('pipe-scope: Unscoped filter runs on all channels', async function (t) {
|
||||
var count = 0;
|
||||
sw.pipe.pre(202, function (ctx) {
|
||||
count++; return ctx;
|
||||
@@ -432,7 +396,7 @@
|
||||
// INTROSPECTION
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
await T.test('sdk', 'introspection', 'sw.pipe.list() shape', async function () {
|
||||
s.test('introspection: sw.pipe.list() shape', async function (t) {
|
||||
var list = sw.pipe.list();
|
||||
T.assertHasKey(list, 'pre', 'pipe.list');
|
||||
T.assertHasKey(list, 'stream', 'pipe.list');
|
||||
@@ -442,7 +406,7 @@
|
||||
T.assert(Array.isArray(list.render), 'render should be array');
|
||||
});
|
||||
|
||||
await T.test('sdk', 'introspection', 'Stats populated after execution', async function () {
|
||||
s.test('introspection: Stats populated after execution', async function (t) {
|
||||
var list = sw.pipe.list();
|
||||
// Find any filter with calls > 0 (previous tests should have run some)
|
||||
var anyRun = false;
|
||||
@@ -454,7 +418,7 @@
|
||||
T.assert(anyRun, 'at least one filter should have calls > 0 from previous tests');
|
||||
});
|
||||
|
||||
await T.test('sdk', 'introspection', 'Stats include avgMs and errors', async function () {
|
||||
s.test('introspection: Stats include avgMs and errors', async function (t) {
|
||||
var list = sw.pipe.list();
|
||||
var entry = list.pre.find(function (f) { return f.calls > 0; });
|
||||
if (!entry) throw new Error('no filter with calls > 0');
|
||||
@@ -464,18 +428,18 @@
|
||||
T.assert(typeof entry.priority === 'number', 'priority should be number');
|
||||
});
|
||||
|
||||
await T.test('sdk', 'introspection', 'Scoped filter shows scope in list', async function () {
|
||||
s.test('introspection: Scoped filter shows scope in list', async function (t) {
|
||||
var list = sw.pipe.list();
|
||||
var scoped = list.pre.find(function (f) { return f.source === '_test-scoped'; });
|
||||
T.assert(scoped, '_test-scoped filter should exist');
|
||||
T.assert(scoped.scope !== null && scoped.scope !== undefined, 'scope should be set');
|
||||
});
|
||||
|
||||
await T.test('sdk', 'introspection', 'Unscoped filter shows null scope', async function () {
|
||||
s.test('introspection: Unscoped filter shows null scope', async function (t) {
|
||||
var list = sw.pipe.list();
|
||||
var unscoped = list.pre.find(function (f) { return f.source === '_test-pre'; });
|
||||
T.assert(unscoped, '_test-pre filter should exist');
|
||||
T.assert(unscoped.scope === null, 'scope should be null for unscoped filter');
|
||||
});
|
||||
};
|
||||
});
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user