diff --git a/src/main/lib.ts b/src/main/lib.ts index e5e587d5..c5b99c8b 100644 --- a/src/main/lib.ts +++ b/src/main/lib.ts @@ -1,7 +1,7 @@ /* istanbul ignore file */ // cache -export type {TwingCache} from "./lib/cache"; +export type {TwingCache, TwingSynchronousCache} from "./lib/cache"; // error export type {TwingError} from "./lib/error"; @@ -16,15 +16,15 @@ export {createTemplateLoadingError} from "./lib/error/loader"; // loader export type { - TwingFilesystemLoader, TwingFilesystemLoaderFilesystem, TwingFilesystemLoaderFilesystemStats + TwingFilesystemLoader, TwingFilesystemLoaderFilesystem, TwingFilesystemLoaderFilesystemStats, TwingSynchronousFilesystemLoader, TwingSynchronousFilesystemLoaderFilesystem } from "./lib/loader/filesystem"; export type {TwingArrayLoader, TwingSynchronousArrayLoader} from "./lib/loader/array"; -export type {TwingChainLoader} from "./lib/loader/chain"; +export type {TwingChainLoader, TwingSynchronousChainLoader} from "./lib/loader/chain"; export type {TwingLoader, TwingSynchronousLoader} from "./lib/loader"; export {createFilesystemLoader, createSynchronousFilesystemLoader} from "./lib/loader/filesystem"; export {createArrayLoader, createSynchronousArrayLoader} from "./lib/loader/array"; -export {createChainLoader} from "./lib/loader/chain"; +export {createChainLoader, createSynchronousChainLoader} from "./lib/loader/chain"; // markup export type {TwingMarkup} from "./lib/markup"; @@ -263,18 +263,18 @@ export {createWithTagHandler} from "./lib/tag-handler/with"; // core export type { - TwingCallable, TwingCallableArgument, TwingCallableWrapperOptions, TwingCallableWrapper + TwingCallable, TwingCallableArgument, TwingCallableWrapperOptions, TwingCallableWrapper, TwingSynchronousCallable, TwingSynchronousCallableWrapper } from "./lib/callable-wrapper"; export {type TwingContext, createContext} from "./lib/context"; -export type {TwingEnvironment, TwingEnvironmentOptions, TwingNumberFormat} from "./lib/environment"; +export type {TwingEnvironment, TwingEnvironmentOptions, TwingNumberFormat, TwingSynchronousEnvironment, TwingSynchronousEnvironmentOptions} from "./lib/environment"; export type { TwingEscapingStrategy, TwingEscapingStrategyHandler, TwingEscapingStrategyResolver } from "./lib/escaping-strategy"; -export type {TwingExecutionContext} from "./lib/execution-context"; -export type {TwingExtension} from "./lib/extension"; +export type {TwingExecutionContext, TwingSynchronousExecutionContext} from "./lib/execution-context"; +export type {TwingExtension, TwingSynchronousExtension} from "./lib/extension"; export type {TwingExtensionSet} from "./lib/extension-set"; -export type {TwingFilter} from "./lib/filter"; -export type {TwingFunction} from "./lib/function"; +export type {TwingFilter, TwingSynchronousFilter} from "./lib/filter"; +export type {TwingFunction, TwingSynchronousFunction} from "./lib/function"; export type {TwingLexer} from "./lib/lexer"; export type {TwingNodeVisitor} from "./lib/node-visitor"; export type { @@ -289,26 +289,25 @@ export type { TwingTemplateAliases, TwingTemplateBlockMap, TwingTemplateBlockHandler, - TwingTemplateMacroHandler + TwingTemplateMacroHandler, + TwingSynchronousTemplateAliases, + TwingSynchronousTemplateBlockHandler, + TwingSynchronousTemplateBlockMap, + TwingSynchronousTemplateMacroHandler } from "./lib/template"; -export type {TwingTest} from "./lib/test"; +export type {TwingTest, TwingSynchronousTest} from "./lib/test"; export type {TwingTokenStream} from "./lib/token-stream"; -export interface TwingTemplate { - execute: import("./lib/template").TwingTemplate["execute"]; - render: import("./lib/template").TwingTemplate["render"]; -} - export {createEnvironment, createSynchronousEnvironment} from "./lib/environment"; export {createExtensionSet} from "./lib/extension-set"; -export {createFilter} from "./lib/filter"; -export {createFunction} from "./lib/function"; +export {createFilter, createSynchronousFilter} from "./lib/filter"; +export {createFunction, createSynchronousFunction} from "./lib/function"; export {createLexer} from "./lib/lexer"; export {createBaseNode, createNode, getChildren, getChildrenCount} from "./lib/node"; export {createOperator} from "./lib/operator"; export {createSandboxSecurityPolicy} from "./lib/sandbox/security-policy"; export {createSource} from "./lib/source"; export {createSourceMapRuntime} from "./lib/source-map-runtime"; -export {createTemplate} from "./lib/template"; -export {type TwingTemplateLoader, createTemplateLoader} from "./lib/template-loader"; -export {createTest} from "./lib/test"; +export {type TwingTemplate, createTemplate, type TwingSynchronousTemplate, createSynchronousTemplate} from "./lib/template"; +export {type TwingTemplateLoader, type TwingSynchronousTemplateLoader, createTemplateLoader, createSynchronousTemplateLoader} from "./lib/template-loader"; +export {createTest, createSynchronousTest} from "./lib/test"; diff --git a/src/main/lib/context.ts b/src/main/lib/context.ts index a40e04bb..de45ffca 100644 --- a/src/main/lib/context.ts +++ b/src/main/lib/context.ts @@ -69,5 +69,3 @@ export const getEntries = (context: Record): IterableIterator<[str export const getValues = (context: Record): Array => { return Object.values(context); }; - -export type TwingContext2 = Map; diff --git a/src/main/lib/environment.ts b/src/main/lib/environment.ts index ef6dffa6..63b004ed 100644 --- a/src/main/lib/environment.ts +++ b/src/main/lib/environment.ts @@ -27,7 +27,7 @@ import {TwingCache, TwingSynchronousCache} from "./cache"; import {createCoreExtension, createSynchronousCoreExtension} from "./extension/core"; import {createAutoEscapeNode, createTemplateLoadingError, type TwingContext} from "../lib"; import {createSynchronousTemplateLoader, createTemplateLoader} from "./template-loader"; -import {createContext, TwingContext2} from "./context"; +import {createContext} from "./context"; import {iterableToMap} from "./helpers/iterator-to-map"; export type TwingNumberFormat = { @@ -165,7 +165,7 @@ export interface TwingSynchronousEnvironment { readonly numberFormat: TwingNumberFormat; readonly filters: Map; readonly functions: Map; - readonly globals: TwingContext2; + readonly globals: Map; readonly loader: TwingSynchronousLoader; readonly sandboxPolicy: TwingSandboxSecurityPolicy; readonly tests: Map; @@ -567,14 +567,14 @@ export const createSynchronousEnvironment = ( }, render: (name, data, options) => { const template = environment.loadTemplate(name); - const context: TwingContext2 = new Map(Object.entries(data)); + const context: Map = new Map(Object.entries(data)); return template.render(environment, context, options); }, renderWithSourceMap: (name, data, options) => { const sourceMapRuntime = createSourceMapRuntime(); - const context: TwingContext2 = new Map(Object.entries(data)); + const context: Map = new Map(Object.entries(data)); const template = environment.loadTemplate(name); const output = template.render(environment, context, { ...options, diff --git a/src/main/lib/execution-context.ts b/src/main/lib/execution-context.ts index 3cf6d01b..99e62ffc 100644 --- a/src/main/lib/execution-context.ts +++ b/src/main/lib/execution-context.ts @@ -1,5 +1,5 @@ import type {TwingTemplate, TwingTemplateAliases, TwingTemplateBlockMap} from "./template"; -import type {TwingContext, TwingContext2} from "./context"; +import type {TwingContext} from "./context"; import type {TwingOutputBuffer} from "./output-buffer"; import type {TwingSourceMapRuntime} from "./source-map-runtime"; import type {TwingEnvironment, TwingSynchronousEnvironment} from "./environment"; @@ -26,7 +26,7 @@ export type TwingExecutionContext = { export type TwingSynchronousExecutionContext = { aliases: TwingSynchronousTemplateAliases; blocks: TwingSynchronousTemplateBlockMap; - context: TwingContext2; + context: Map; environment: TwingSynchronousEnvironment; nodeExecutor: TwingSynchronousNodeExecutor; outputBuffer: TwingOutputBuffer; diff --git a/src/main/lib/extension/core/functions/include.ts b/src/main/lib/extension/core/functions/include.ts index 02f2108f..420dcc7d 100644 --- a/src/main/lib/extension/core/functions/include.ts +++ b/src/main/lib/extension/core/functions/include.ts @@ -1,6 +1,6 @@ import {isTraversable} from "../../../helpers/is-traversable"; import {isPlainObject} from "../../../helpers/is-plain-object"; -import {createContext, TwingContext2} from "../../../context"; +import {createContext} from "../../../context"; import {createMarkup, TwingMarkup} from "../../../markup"; import type {TwingSynchronousTemplate, TwingTemplate} from "../../../template"; import type {TwingCallable, TwingSynchronousCallable} from "../../../callable-wrapper"; @@ -103,7 +103,7 @@ export const include: TwingCallable<[ export const includeSynchronously: TwingSynchronousCallable<[ templates: string | TwingSynchronousTemplate | null | Array, - variables: TwingContext2, + variables: Map, withContext: boolean, ignoreMissing: boolean, sandboxed: boolean diff --git a/src/main/lib/helpers/get-constant.ts b/src/main/lib/helpers/get-constant.ts index 2f877f4e..88b576c6 100644 --- a/src/main/lib/helpers/get-constant.ts +++ b/src/main/lib/helpers/get-constant.ts @@ -8,9 +8,9 @@ * * @returns {any} */ -import {TwingContext, TwingContext2} from "../context"; +import {TwingContext} from "../context"; -export function getConstant(context: TwingContext | TwingContext2, name: string, object: any | null): any { +export function getConstant(context: TwingContext | Map, name: string, object: any | null): any { if (object) { return object[name]; } else { diff --git a/src/main/lib/helpers/get-context-value.ts b/src/main/lib/helpers/get-context-value.ts index a844ec9f..9e26593f 100644 --- a/src/main/lib/helpers/get-context-value.ts +++ b/src/main/lib/helpers/get-context-value.ts @@ -1,4 +1,4 @@ -import type {TwingContext, TwingContext2} from "../context"; +import type {TwingContext} from "../context"; export const getContextValue = ( charset: string, @@ -51,8 +51,8 @@ export const getContextValueSynchronously = ( charset: string, templateName: string, isStrictVariables: boolean, - context: TwingContext2, - globals: TwingContext2, + context: Map, + globals: Map, name: string, isAlwaysDefined: boolean, shouldIgnoreStrictCheck: boolean, diff --git a/src/main/lib/loader/chain.ts b/src/main/lib/loader/chain.ts index 4272745b..878bf498 100644 --- a/src/main/lib/loader/chain.ts +++ b/src/main/lib/loader/chain.ts @@ -1,4 +1,4 @@ -import type {TwingLoader} from "../loader"; +import type {TwingLoader, TwingSynchronousLoader} from "../loader"; import type {TwingSource} from "../source"; export interface TwingChainLoader extends TwingLoader { @@ -7,6 +7,12 @@ export interface TwingChainLoader extends TwingLoader { addLoader(loader: TwingLoader): void; } +export interface TwingSynchronousChainLoader extends TwingSynchronousLoader { + readonly loaders: Array; + + addLoader(loader: TwingSynchronousLoader): void; +} + export const createChainLoader = ( loaders: Array ): TwingChainLoader => { @@ -139,3 +145,125 @@ export const createChainLoader = ( return loader; }; + +export const createSynchronousChainLoader = ( + loaders: Array +): TwingSynchronousChainLoader => { + let existsCache: Map = new Map(); + + const addLoader: TwingSynchronousChainLoader["addLoader"] = (loader) => { + loaders.push(loader); + existsCache = new Map(); + }; + + const loader: TwingSynchronousChainLoader = { + get loaders() { + return loaders + }, + addLoader, + exists: (name, from) => { + const cachedResult = existsCache.get(name); + + if (cachedResult) { + return cachedResult; + } + + const existsAtIndex = (index: number): boolean => { + if (index < loaders.length) { + const loader = loaders[index]; + + const exists = loader.exists(name, from); + + existsCache.set(name, exists); + + if (!exists) { + return existsAtIndex(index + 1); + } else { + return true; + } + } else { + return false; + } + }; + + const exists = existsAtIndex(0); + + existsCache.set(name, exists); + + return exists; + }, + resolve: (name, from) => { + const resolveAtIndex = (index: number): string | null => { + if (index < loaders.length) { + const loader = loaders[index]; + + const exists = loader.exists(name, from); + + const key = exists ? loader.resolve(name, from) : resolveAtIndex(index + 1); + + if (key === null) { + return resolveAtIndex(index + 1); + } + + return key; + } else { + return null; + } + }; + + const key = resolveAtIndex(0); + + if (key) { + return key; + } else { + return null; + } + }, + getSource: (name, from) => { + const getSourceContextAtIndex = (index: number): TwingSource | null => { + if (index < loaders.length) { + let loader = loaders[index]; + + const source = loader.getSource(name, from); + + if (source === null) { + return getSourceContextAtIndex(index + 1); + } + + return source; + } else { + return null; + } + }; + + const source = getSourceContextAtIndex(0); + + if (source) { + return source; + } else { + return null; + } + }, + isFresh: (name, time, from) => { + const isFreshAtIndex = (index: number): boolean | null => { + if (index < loaders.length) { + const loader = loaders[index]; + + const isFresh = loader.isFresh(name, time, from); + + if (isFresh === null) { + return isFreshAtIndex(index + 1); + } + + return isFresh; + } else { + return null; + } + }; + + return isFreshAtIndex(0); + } + }; + + return loader; +}; diff --git a/src/main/lib/node-executor/for.ts b/src/main/lib/node-executor/for.ts index c77e40e2..df7a9893 100644 --- a/src/main/lib/node-executor/for.ts +++ b/src/main/lib/node-executor/for.ts @@ -1,6 +1,6 @@ import {TwingNodeExecutor, TwingSynchronousNodeExecutor} from "../node-executor"; import {TwingForNode} from "../node/for"; -import {TwingContext, TwingContext2} from "../context"; +import {TwingContext} from "../context"; import {ensureTraversable} from "../helpers/ensure-traversable"; import {count} from "../helpers/count"; import {iterate, iterateSynchronously} from "../helpers/iterate"; @@ -140,7 +140,7 @@ export const executeForNodeSynchronously: TwingSynchronousNodeExecutor = context.get('_parent'); context.delete('_seq'); context.delete('_iterated'); diff --git a/src/main/lib/node-executor/with.ts b/src/main/lib/node-executor/with.ts index f10f9f22..6d61c1ee 100644 --- a/src/main/lib/node-executor/with.ts +++ b/src/main/lib/node-executor/with.ts @@ -1,6 +1,6 @@ import {TwingNodeExecutor, TwingSynchronousNodeExecutor} from "../node-executor"; import {TwingWithNode} from "../node/with"; -import {createContext, TwingContext, TwingContext2} from "../context"; +import {createContext, TwingContext} from "../context"; import {createRuntimeError} from "../error/runtime"; import {mergeIterables} from "../helpers/merge-iterables"; import {iteratorToMap} from "../helpers/iterator-to-map"; @@ -47,7 +47,7 @@ export const executeWithNodeSynchronously: TwingSynchronousNodeExecutor; if (variablesNode) { let variables = execute(variablesNode, executionContext); diff --git a/src/main/lib/sandbox/security-policy.ts b/src/main/lib/sandbox/security-policy.ts index 12f1e32d..3028d78e 100644 --- a/src/main/lib/sandbox/security-policy.ts +++ b/src/main/lib/sandbox/security-policy.ts @@ -5,7 +5,7 @@ export interface TwingSandboxSecurityPolicy { * @param {any | TwingMarkup} candidate * @param {string} method * - * @throws {@link TwingSandboxSecurityNotAllowedMethodError} When the method is not allowed on the passed object + * @throws When the method is not allowed on the passed object */ checkMethodAllowed(candidate: any | TwingMarkup, method: string): void; diff --git a/src/main/lib/template.ts b/src/main/lib/template.ts index b50cc813..d5c8c2f8 100644 --- a/src/main/lib/template.ts +++ b/src/main/lib/template.ts @@ -1,4 +1,4 @@ -import {createContext, TwingContext, TwingContext2} from "./context"; +import {createContext, TwingContext} from "./context"; import {TwingEnvironment, TwingSynchronousEnvironment} from "./environment"; import {createOutputBuffer, TwingOutputBuffer} from "./output-buffer"; import {TwingSourceMapRuntime} from "./source-map-runtime"; @@ -184,7 +184,7 @@ export interface TwingSynchronousTemplate { */ execute( environment: TwingSynchronousEnvironment, - context: TwingContext2, + context: Map, blocks: TwingSynchronousTemplateBlockMap, outputBuffer: TwingOutputBuffer, options?: { @@ -225,7 +225,7 @@ export interface TwingSynchronousTemplate { render( environment: TwingSynchronousEnvironment, - context: TwingContext2, + context: Map, options?: { nodeExecutor?: TwingSynchronousNodeExecutor; outputBuffer?: TwingOutputBuffer; @@ -749,7 +749,7 @@ export const createSynchronousTemplate = ( const aliases = {...template.aliases}; - const localVariables: TwingContext2 = new Map(); + const localVariables: Map = new Map(); for (const {key: keyNode, value: defaultValueNode} of keyValuePairs) { const key = keyNode.attributes.value as string; diff --git a/src/test/tests/unit/lib.ts b/src/test/tests/unit/lib.ts index 774148b6..27539a56 100644 --- a/src/test/tests/unit/lib.ts +++ b/src/test/tests/unit/lib.ts @@ -9,7 +9,7 @@ tape('library index', ({same, end}) => { 'createParsingError', 'createFilesystemLoader', 'createSynchronousFilesystemLoader', 'createArrayLoader', 'createSynchronousArrayLoader', - 'createChainLoader', + 'createChainLoader', 'createSynchronousChainLoader', 'createMarkup', 'isAMarkup', 'createApplyNode', 'createAutoEscapeNode', @@ -117,17 +117,17 @@ tape('library index', ({same, end}) => { 'getChildrenCount', 'createEnvironment', 'createSynchronousEnvironment', 'createExtensionSet', - 'createFilter', - 'createFunction', + 'createFilter', 'createSynchronousFilter', + 'createFunction', 'createSynchronousFunction', 'createLexer', 'createOperator', 'createSandboxSecurityPolicy', 'createSource', 'createSourceMapRuntime', - 'createTemplate', - 'createTest', + 'createTemplate', 'createSynchronousTemplate', + 'createTest', 'createSynchronousTest', 'executeNode', 'executeNodeSynchronously', - 'createTemplateLoader', + 'createTemplateLoader', 'createSynchronousTemplateLoader', 'createContext', 'createOutputBuffer' ]; diff --git a/src/test/tests/unit/lib/loader/chain/index.ts b/src/test/tests/unit/lib/loader/chain/index.ts index cdcee919..22023510 100644 --- a/src/test/tests/unit/lib/loader/chain/index.ts +++ b/src/test/tests/unit/lib/loader/chain/index.ts @@ -1,6 +1,6 @@ import * as tape from 'tape'; -import {createChainLoader} from "../../../../../../main/lib/loader/chain"; -import {createArrayLoader} from "../../../../../../main/lib/loader/array"; +import {createChainLoader, createSynchronousChainLoader} from "../../../../../../main/lib/loader/chain"; +import {createArrayLoader, createSynchronousArrayLoader} from "../../../../../../main/lib/loader/array"; import {spy, stub} from "sinon"; tape('createChainLoader', ({test}) => { @@ -267,3 +267,266 @@ tape('createChainLoader', ({test}) => { }); }); }); + +tape('createSynchronousChainLoader', ({test}) => { + test('getSourceContext', ({test}) => { + let loader = createSynchronousChainLoader([ + createSynchronousArrayLoader({'foo': 'bar'}), + createSynchronousArrayLoader({'errors/index.html': 'baz'}) + ]); + + test('return the source context of the first loader that returns a source context', ({test}) => { + test('foo', ({same, end}) => { + const source = loader.getSource('foo', null); + + same(source?.name, 'foo'); + same(source?.code, 'bar'); + + end(); + }); + + test('errors/index.html', ({same, end}) => { + const source = loader.getSource('errors/index.html', null); + + same(source?.name, 'errors/index.html'); + same(source?.code, 'baz'); + + end(); + }); + }); + + test('returns null when the template does not exist', ({same, end}) => { + const loader = createSynchronousChainLoader([]); + + same(loader.getSource('foo', null), null); + + end(); + }); + }); + + test('resolve', ({test}) => { + test('returns the template FQN when the template exists', ({same, end}) => { + let loader = createSynchronousChainLoader([ + createSynchronousArrayLoader({'foo': 'bar'}), + createSynchronousArrayLoader({'foo': 'foobar', 'bar': 'foo'}), + ]); + + same(loader.resolve('foo', null), 'foo'); + same(loader.resolve('bar', null), 'bar'); + + let resolveStub = stub(loader, 'resolve').returns(null); + + loader = createSynchronousChainLoader([ + loader + ]); + + same(loader.resolve('foo', null), null); + + resolveStub.restore(); + + let loader2 = createSynchronousArrayLoader({'foo': 'bar'}); + + resolveStub = stub(loader2, 'resolve').returns(null); + + loader = createSynchronousChainLoader([ + loader2 + ]); + + same(loader.resolve('foo', null), null); + + resolveStub.restore(); + + end(); + }); + + test('returns null when the template does not exist', ({same, end}) => { + const loader = createSynchronousChainLoader([]); + + same(loader.resolve('foo', null), null); + + end(); + }); + }); + + test('addLoader', ({same, end}) => { + const loader = createSynchronousChainLoader([]); + + loader.addLoader(createSynchronousArrayLoader({'foo': 'bar'})); + + const source = loader.getSource('foo', null); + + same(source?.code, 'bar'); + + end(); + }); + + test('getLoaders', (test) => { + let loaders = [ + createArrayLoader({'foo': 'bar'}) + ]; + + let loader = createChainLoader(loaders); + + test.same(loader.loaders, loaders); + + test.end(); + }); + + test('exists', ({test}) => { + let loader1 = createSynchronousArrayLoader({}); + let loader1ExistsStub = stub(loader1, 'exists').returns(false); + let loader1GetSourceSpy = spy(loader1, 'getSource'); + + let loader2 = createSynchronousArrayLoader({}); + let loader2ExistsStub = stub(loader2, 'exists').returns(true); + let loader2GetSourceSpy = spy(loader2, 'getSource'); + + let loader3 = createSynchronousArrayLoader({}); + let loader3ExistsStub = stub(loader3, 'exists').returns(true); + let loader3GetSourceSpy = spy(loader3, 'getSource'); + + test('resolves to true as soon as a loader resolves to true', ({same, end}) => { + let loader = createSynchronousChainLoader([ + loader1, + loader2, + loader3 + ]); + + same(loader.exists('foo', null), true); + same(loader1ExistsStub.callCount, 1, 'loader 1 exists is called once'); + same(loader2ExistsStub.callCount, 1, 'loader 2 exists is called once'); + same(loader3ExistsStub.callCount, 0, 'loader 3 exists is not called'); + same(loader1GetSourceSpy.callCount, 0, 'loader 1 getSourceContext is not called'); + same(loader2GetSourceSpy.callCount, 0, 'loader 2 getSourceContext is not called'); + same(loader3GetSourceSpy.callCount, 0, 'loader 3 getSourceContext is not called'); + + loader1ExistsStub.restore(); + loader2ExistsStub.restore(); + loader3ExistsStub.restore(); + + end(); + }); + + test('resolves to false is all loaders resolve to false', ({same, end}) => { + let loader = createSynchronousChainLoader([ + loader1, + loader2 + ]); + + loader1ExistsStub = stub(loader1, 'exists').returns(false); + loader2ExistsStub = stub(loader2, 'exists').returns(false); + + same(loader.exists('foo', null), false); + + loader1ExistsStub.restore(); + loader2ExistsStub.restore(); + + end(); + }); + + test('hits cache on subsequent calls', async ({same, end}) => { + let loader = createSynchronousChainLoader([ + createSynchronousArrayLoader({ + foo: 'foo' + }) + ]); + + const existsSpy = spy(loader.loaders[0], 'exists'); + + await loader.exists('foo', null); + await loader.exists('foo', null); + + same(existsSpy.callCount, 1); + + existsSpy.restore(); + + end(); + }); + }); + + test('isFresh', ({same, end}) => { + let loader = createSynchronousChainLoader([ + createSynchronousArrayLoader({'foo': 'bar'}), + createSynchronousArrayLoader({'foo': 'foobar', 'bar': 'foo'}), + ]); + + same(loader.isFresh('foo', 0, null), true); + same(loader.isFresh('bar', 0, null), true); + + let isFreshStub = stub(loader, 'isFresh').returns(null); + + loader = createSynchronousChainLoader([ + loader + ]); + + same(loader.isFresh('foo', 0, null), null); + + isFreshStub.restore(); + + const loader2 = createSynchronousArrayLoader({'foo': 'bar'}); + + isFreshStub = stub(loader2, 'isFresh').returns(null); + + loader = createSynchronousChainLoader([ + loader2 + ]); + + same(loader.isFresh('foo', 0, null), null); + + isFreshStub.restore(); + + end(); + }); + + test('resolve', ({test}) => { + test('returns whatever the first loader that handles the passed name returns', ({same, end}) => { + const loader = createSynchronousChainLoader([ + createSynchronousArrayLoader({'foo': 'bar'}), + createSynchronousArrayLoader({'bar': 'foo'}), + ]); + + same(loader.resolve('bar', null), 'bar'); + + end(); + }); + + test('when some loaders return null', ({same, end}) => { + const loader1 = createSynchronousArrayLoader({}); + + stub(loader1, 'exists').returns(true); + stub(loader1, 'resolve').returns(null); + + const loader2 = createSynchronousArrayLoader({'bar': 'foo'}); + + const loader = createSynchronousChainLoader([ + loader1, + loader2 + ]); + + same(loader.resolve('bar', null), 'bar'); + + end(); + }); + + test('when all loaders return null', ({same, end}) => { + const loader1 = createSynchronousArrayLoader({}); + + stub(loader1, 'exists').returns(true); + stub(loader1, 'resolve').returns(null); + + const loader2 = createSynchronousArrayLoader({}); + + stub(loader2, 'exists').returns(true); + stub(loader2, 'resolve').returns(null); + + const loader = createSynchronousChainLoader([ + loader1, + loader2 + ]); + + same(loader.resolve('foo', null), null); + + end(); + }); + }); +});