Skip to content

Commit

Permalink
refacto(di): add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
lorenzofox3 committed Feb 27, 2024
1 parent 38d338c commit 64def71
Show file tree
Hide file tree
Showing 9 changed files with 379 additions and 20 deletions.
9 changes: 6 additions & 3 deletions packages/di/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,19 @@
}
},
"scripts": {
"dev": "vite",
"test": "node test/run-ci.js"
},
"prettier": {
"singleQuote": true
"author": "Laurent RENARD",
"devDependencies": {
"@cofn/core": "workspace:^",
"@cofn/test-lib": "workspace:*"
},
"keywords": [
"di",
"cofn",
"web",
"ui"
],
"author": "Laurent RENARD",
"license": "MIT"
}
21 changes: 5 additions & 16 deletions packages/di/src/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { createInjector, factorify } from './injector.js';

const rootRegistry = {};
const registrySymbol = Symbol('registry');

const factorify = (factoryLike) =>
typeof factoryLike === 'function' ? factoryLike : () => factoryLike;

export const provide = (providerFn) => (comp) => {
const _providerFn = factorify(providerFn);
return function* ({ $host, ...rest }) {
Expand All @@ -30,19 +28,10 @@ export const provide = (providerFn) => (comp) => {
export const inject = (comp) =>
function* ({ $host, ...rest }) {
yield; // The element must be mounted, so we can look up the DOM tree
const services =
$host.closest('[provider]')?.[registrySymbol] ?? rootRegistry;
const proxy = new Proxy(services, {
get(target, prop, receiver) {
if (!Reflect.has(services, prop)) {
throw new Error(`could not resolve injectable "${prop}"`);
}
const factory = factorify(services[prop]);
return factory(proxy);
},
const services = createInjector({
services: $host.closest('[provider]')?.[registrySymbol] ?? rootRegistry,
});

const instance = comp({ $host, services: proxy, ...rest });
const instance = comp({ $host, services, ...rest });
instance.next();
yield* instance;
};
15 changes: 15 additions & 0 deletions packages/di/src/injector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const factorify = (factoryLike) =>
typeof factoryLike === 'function' ? factoryLike : () => factoryLike;
export const createInjector = ({ services }) => {
const proxy = new Proxy(services, {
get(target, prop, receiver) {
if (!Reflect.has(services, prop)) {
throw new Error(`could not resolve injectable "${prop}"`);
}
const factory = factorify(services[prop]);
return factory(proxy);
},
});

return proxy;
};
129 changes: 129 additions & 0 deletions packages/di/test/dom-tree.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { test } from '@cofn/test-lib/client';
import { define } from '@cofn/core';
import { inject, provide } from '../src/index.js';
import { nextTick } from './utils.js';

const debug = document.getElementById('debug');
const dumb = function* () {};

define('test-dumb-child', dumb);

test('DOM element that registers some injectables has "provider" attribute', async ({
eq,
}) => {
define(
'test-provider',
provide({
a: 'a',
})(dumb),
);

const root = document.createElement('test-provider');
root.append(document.createElement('test-dumb-child'));

debug.append(root);

await nextTick();

eq(root.hasAttribute('provider'), true);
});
test('"inject" injects services which were registered by a parent "provider element" ', async ({
eq,
}) => {
define(
'test-provider-1',
provide({
a: 'a',
b: 'b',
})(dumb),
);
define(
'test-consumer',
inject(function* ({ $host, services }) {
$host.a = services.a;
$host.b = services.b;
}),
);

const root = document.createElement('test-provider-1');
const anyChild = document.createElement('test-dumb-child');
const injected = document.createElement('test-consumer');

anyChild.appendChild(injected);
root.append(anyChild);
debug.append(root);

await nextTick();

eq(injected.a, 'a');
eq(injected.b, 'b');
});

test('provider can be a function which has the $host as a parameter', async ({
eq,
}) => {
define(
'test-provider-fn',
provide(({ $host }) => {
return {
injected: $host.getAttribute('woot'),
};
})(dumb),
);
define(
'test-consumer-fn',
inject(function* ({ $host, services }) {
$host.injected = services.injected;
}),
);

const root = document.createElement('test-provider-fn');
root.setAttribute('woot', 'bar');
const anyChild = document.createElement('test-dumb-child');
const injected = document.createElement('test-consumer-fn');

anyChild.appendChild(injected);
root.append(anyChild);
debug.append(root);

await nextTick();

eq(injected.injected, 'bar');
});

test(`provider element shadows parent's injectables`, async ({ eq }) => {
define(
'test-provider-root',
provide({
a: 'a',
b: 'b',
})(dumb),
);
define(
'test-provider-child',
provide({
a: 'abis',
})(dumb),
);

define(
'test-consumer-shadowed',
inject(function* ({ $host, services }) {
$host.a = services.a;
$host.b = services.b;
}),
);

const root = document.createElement('test-provider-root');
const anyChild = document.createElement('test-provider-child');
const injected = document.createElement('test-consumer-shadowed');

anyChild.appendChild(injected);
root.append(anyChild);
debug.append(root);

await nextTick();

eq(injected.a, 'abis');
eq(injected.b, 'b');
});
122 changes: 122 additions & 0 deletions packages/di/test/injector.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { test } from '@cofn/test-lib/client';
import { createInjector } from '../src/injector.js';

test('instantiates an injectable, calling the factory', ({ eq }) => {
const { a } = createInjector({
services: {
a: () => 'a',
},
});

eq(a, 'a');
});

test('instantiates an injectable, when it is a value', ({ eq }) => {
const { a } = createInjector({
services: {
a: 'a',
},
});

eq(a, 'a');
});

test('everytime the getter is called a new instance is created', ({
eq,
isNot,
}) => {
const services = createInjector({
injectables: {
a: () => ({ prop: 'a' }),
},
});
const instance1 = services.a;
const { a: instance2 } = services;
eq(instance1, { prop: 'a' });
eq(instance2, { prop: 'a' });
isNot(instance2, instance1);
});

test('resolves dependency graph, instantiating the transitive dependencies ', ({
eq,
}) => {
const services = createInjector({
injectables: {
a: ({ b, c }) => b + '+' + c,
b: () => 'b',
c: ({ d }) => d,
d: 'd',
},
});
eq(services.a, 'b+d');
});

test('injection tokens can be symbols', ({ eq }) => {
const aSymbol = Symbol('a');
const bSymbol = Symbol('b');
const cSymbol = Symbol('c');
const dSymbol = Symbol('d');

const services = createInjector({
services: {
[aSymbol]: ({ [bSymbol]: b, [cSymbol]: c }) => b + '+' + c,
[bSymbol]: () => 'b',
[cSymbol]: ({ [dSymbol]: d }) => d,
[dSymbol]: 'd',
},
});
eq(services[aSymbol], 'b+d');
});

test(`only instantiates an injectable when required`, ({ eq, notOk, ok }) => {
let aInstantiated = false;
let bInstantiated = false;
let cInstantiated = false;

const services = createInjector({
services: {
a: ({ b }) => {
aInstantiated = true;
return b;
},
b: () => {
bInstantiated = true;
return 'b';
},
c: () => {
cInstantiated = true;
return 'c';
},
},
});

const { a } = services;

eq(a, 'b');
ok(aInstantiated);
ok(bInstantiated);
notOk(cInstantiated);

const { c } = services;
eq(c, 'c');
ok(cInstantiated);
});

test('gives a friendly message when it can not resolve a dependency', ({
eq,
fail,
}) => {
const services = createInjector({
services: {
a: ({ b }) => b,
b: ({ c }) => c,
},
});

try {
const { a } = services;
fail('should not reach that statement');
} catch (err) {
eq(err.message, 'could not resolve injectable "c"');
}
});
63 changes: 63 additions & 0 deletions packages/di/test/run-ci.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { createServer } from 'vite';
import { firefox, chromium, webkit } from 'playwright';

const PORT = 3002;
const TIMEOUT = 30_000;

(async () => {
let server,
browsers = [];
const browserList = [firefox, chromium, webkit];
try {
server = await createServer({
server: {
port: PORT,
},
});
await server.listen();

browsers = await Promise.all(
browserList.map((browserApp) => browserApp.launch({ headless: true })),
);

await Promise.all(
browsers.map((browser) => {
console.log(browser._name);
return new Promise((resolve, reject) => {
let timerId;
Promise.race([
browser
.newPage()
.then((page) => {
page.on('websocket', (webSocket) => {
webSocket.on('framesent', ({ payload }) => {
const asJson = JSON.parse(payload);
if (asJson?.data?.type === 'STREAM_ENDED') {
clearTimeout(timerId);
resolve();
}
});
});

return page.goto(
`http://localhost:${PORT}/test/test-suite.html`,
);
})
.catch(reject),
new Promise((resolve, reject) => {
timerId = setTimeout(() => reject('timeout'), TIMEOUT);
}),
]);
});
}),
);
} catch (e) {
console.error(e);
process.exitCode = 1;
} finally {
await Promise.all(browsers.map((browser) => browser.close()));
if (server) {
await server.close();
}
}
})();
Loading

0 comments on commit 64def71

Please sign in to comment.