diff --git a/src/generators/Generator.ts b/src/generators/Generator.ts index 3cd6d5da25..323af454c7 100644 --- a/src/generators/Generator.ts +++ b/src/generators/Generator.ts @@ -728,6 +728,16 @@ export default class Generator { } } + if (node.type === 'AwaitBlock') { + node.metadata = contextualise(node.expression, contextDependencies, indexes); + + contextDependencies = new Map(contextDependencies); + contextDependencies.set(node.value, node.metadata.dependencies); + contextDependencies.set(node.error, node.metadata.dependencies); + + contextDependenciesStack.push(contextDependencies); + } + if (node.type === 'IfBlock') { node.metadata = contextualise(node.expression, contextDependencies, indexes); } diff --git a/src/generators/dom/preprocess.ts b/src/generators/dom/preprocess.ts index 5778258155..62768184a0 100644 --- a/src/generators/dom/preprocess.ts +++ b/src/generators/dom/preprocess.ts @@ -108,6 +108,59 @@ const preprocessors = { node.var = block.getUniqueName(`text`); }, + AwaitBlock: ( + generator: DomGenerator, + block: Block, + state: State, + node: Node, + inEachBlock: boolean, + elementStack: Node[], + componentStack: Node[], + stripWhitespace: boolean, + nextSibling: Node + ) => { + cannotUseInnerHTML(node); + + node.var = block.getUniqueName('await_block'); + block.addDependencies(node.metadata.dependencies); + + let dynamic = false; + + [ + ['pending', null], + ['then', node.value], + ['catch', node.error] + ].forEach(([status, arg]) => { + const child = node[status]; + + const context = block.getUniqueName(arg || '_'); + const contexts = new Map(block.contexts); + contexts.set(arg, context); + + child._block = block.child({ + comment: createDebuggingComment(child, generator), + name: generator.getUniqueName(`create_${status}_block`), + params: block.params.concat(context), + context, + contexts + }); + + child._state = getChildState(state); + + preprocessChildren(generator, child._block, child._state, child, inEachBlock, elementStack, componentStack, stripWhitespace, nextSibling); + generator.blocks.push(child._block); + + if (child._block.dependencies.size > 0) { + dynamic = true; + block.addDependencies(child._block.dependencies); + } + }); + + node.pending._block.hasUpdateMethod = dynamic; + node.then._block.hasUpdateMethod = dynamic; + node.catch._block.hasUpdateMethod = dynamic; + }, + IfBlock: ( generator: DomGenerator, block: Block, diff --git a/src/generators/dom/visitors/AwaitBlock.ts b/src/generators/dom/visitors/AwaitBlock.ts new file mode 100644 index 0000000000..c0d5c25b85 --- /dev/null +++ b/src/generators/dom/visitors/AwaitBlock.ts @@ -0,0 +1,155 @@ +import deindent from '../../../utils/deindent'; +import visit from '../visit'; +import { DomGenerator } from '../index'; +import Block from '../Block'; +import isDomNode from './shared/isDomNode'; +import { Node } from '../../../interfaces'; +import { State } from '../interfaces'; + +export default function visitAwaitBlock( + generator: DomGenerator, + block: Block, + state: State, + node: Node, + elementStack: Node[], + componentStack: Node[] +) { + const name = node.var; + + const needsAnchor = node.next ? !isDomNode(node.next, generator) : !state.parentNode || !isDomNode(node.parent, generator); + const anchor = needsAnchor + ? block.getUniqueName(`${name}_anchor`) + : (node.next && node.next.var) || 'null'; + + const params = block.params.join(', '); + + block.contextualise(node.expression); + const { snippet } = node.metadata; + + if (needsAnchor) { + block.addElement( + anchor, + `@createComment()`, + `@createComment()`, + state.parentNode + ); + } + + const promise = block.getUniqueName(`promise`); + const resolved = block.getUniqueName(`resolved`); + const await_block = block.getUniqueName(`await_block`); + const await_block_type = block.getUniqueName(`await_block_type`); + const token = block.getUniqueName(`token`); + const await_token = block.getUniqueName(`await_token`); + const handle_promise = block.getUniqueName(`handle_promise`); + const replace_await_block = block.getUniqueName(`replace_await_block`); + const old_block = block.getUniqueName(`old_block`); + const value = block.getUniqueName(`value`); + const error = block.getUniqueName(`error`); + const create_pending_block = node.pending._block.name; + const create_then_block = node.then._block.name; + const create_catch_block = node.catch._block.name; + + block.addVariable(await_block); + block.addVariable(await_block_type); + block.addVariable(await_token); + block.addVariable(promise); + block.addVariable(resolved); + + block.builders.init.addBlock(deindent` + function ${replace_await_block}(${token}, type, ${value}, ${params}) { + if (${token} !== ${await_token}) return; + + var ${old_block} = ${await_block}; + ${await_block} = (${await_block_type} = type)(${params}, ${resolved} = ${value}, #component); + + if (${old_block}) { + ${old_block}.u(); + ${old_block}.d(); + ${await_block}.c(); + ${await_block}.m(${anchor}.parentNode, ${anchor}); + } + } + + function ${handle_promise}(${promise}, ${params}) { + var ${token} = ${await_token} = {}; + + if (@isPromise(${promise})) { + ${promise}.then(function(${value}) { + ${replace_await_block}(${token}, ${create_then_block}, ${value}, ${params}); + }, function (${error}) { + ${replace_await_block}(${token}, ${create_catch_block}, ${error}, ${params}); + }); + + // if we previously had a then/catch block, destroy it + if (${await_block_type} !== ${create_pending_block}) { + ${replace_await_block}(${token}, ${create_pending_block}, null, ${params}); + return true; + } + } else { + ${resolved} = ${promise}; + if (${await_block_type} !== ${create_then_block}) { + ${replace_await_block}(${token}, ${create_then_block}, ${resolved}, ${params}); + return true; + } + } + } + + ${handle_promise}(${promise} = ${snippet}, ${params}); + `); + + block.builders.create.addBlock(deindent` + ${await_block}.c(); + `); + + block.builders.claim.addBlock(deindent` + ${await_block}.l(${state.parentNodes}); + `); + + const targetNode = state.parentNode || '#target'; + const anchorNode = state.parentNode ? 'null' : 'anchor'; + + block.builders.mount.addBlock(deindent` + ${await_block}.m(${targetNode}, ${anchorNode}); + `); + + const conditions = []; + if (node.metadata.dependencies) { + conditions.push( + `(${node.metadata.dependencies.map(dep => `'${dep}' in changed`).join(' || ')})` + ); + } + + conditions.push( + `${promise} !== (${promise} = ${snippet})`, + `${handle_promise}(${promise}, ${params})` + ); + + if (node.pending._block.hasUpdateMethod) { + block.builders.update.addBlock(deindent` + if (${conditions.join(' && ')}) { + // nothing + } else { + ${await_block}.p(changed, ${params}, ${resolved}); + } + `); + } else { + block.builders.update.addBlock(deindent` + if (${conditions.join(' && ')}) { + ${await_block}.c(); + ${await_block}.m(${anchor}.parentNode, ${anchor}); + } + `); + } + + block.builders.destroy.addBlock(deindent` + ${await_token} = null; + ${await_block}.d(); + `); + + [node.pending, node.then, node.catch].forEach(status => { + status.children.forEach(child => { + visit(generator, status._block, status._state, child, elementStack, componentStack); + }); + }); +} \ No newline at end of file diff --git a/src/generators/dom/visitors/index.ts b/src/generators/dom/visitors/index.ts index 74a272d1dd..fb0da9e0bc 100644 --- a/src/generators/dom/visitors/index.ts +++ b/src/generators/dom/visitors/index.ts @@ -1,3 +1,4 @@ +import AwaitBlock from './AwaitBlock'; import EachBlock from './EachBlock'; import Element from './Element/Element'; import IfBlock from './IfBlock'; @@ -7,6 +8,7 @@ import Text from './Text'; import { Visitor } from '../interfaces'; const visitors: Record = { + AwaitBlock, EachBlock, Element, IfBlock, diff --git a/src/generators/server-side-rendering/index.ts b/src/generators/server-side-rendering/index.ts index c6741cb958..f9c40ddb08 100644 --- a/src/generators/server-side-rendering/index.ts +++ b/src/generators/server-side-rendering/index.ts @@ -165,16 +165,29 @@ export default function ssr( }; }; - var escaped = { - '"': '"', - "'": '&##39;', - '&': '&', - '<': '<', - '>': '>' - }; + ${ + // TODO this is a bit hacky + /__escape/.test(generator.renderCode) && deindent` + var escaped = { + '"': '"', + "'": '&##39;', + '&': '&', + '<': '<', + '>': '>' + }; + + function __escape(html) { + return String(html).replace(/["'&<>]/g, match => escaped[match]); + } + ` + } - function __escape(html) { - return String(html).replace(/["'&<>]/g, match => escaped[match]); + ${ + /__isPromise/.test(generator.renderCode) && deindent` + function __isPromise(value) { + return value && typeof value.then === 'function'; + } + ` } `.replace(/(@+|#+|%+)(\w*(?:-\w*)?)/g, (match: string, sigil: string, name: string) => { if (sigil === '@') return generator.alias(name); diff --git a/src/generators/server-side-rendering/preprocess.ts b/src/generators/server-side-rendering/preprocess.ts index 0734a135f2..7e9cb7365d 100644 --- a/src/generators/server-side-rendering/preprocess.ts +++ b/src/generators/server-side-rendering/preprocess.ts @@ -10,6 +10,16 @@ const preprocessors = { RawMustacheTag: noop, Text: noop, + AwaitBlock: ( + generator: SsrGenerator, + node: Node, + elementStack: Node[] + ) => { + preprocessChildren(generator, node.pending, elementStack); + preprocessChildren(generator, node.then, elementStack); + preprocessChildren(generator, node.catch, elementStack); + }, + IfBlock: ( generator: SsrGenerator, node: Node, diff --git a/src/generators/server-side-rendering/visitors/AwaitBlock.ts b/src/generators/server-side-rendering/visitors/AwaitBlock.ts new file mode 100644 index 0000000000..6251eb15c0 --- /dev/null +++ b/src/generators/server-side-rendering/visitors/AwaitBlock.ts @@ -0,0 +1,40 @@ +import visit from '../visit'; +import { SsrGenerator } from '../index'; +import Block from '../Block'; +import { Node } from '../../../interfaces'; + +export default function visitAwaitBlock( + generator: SsrGenerator, + block: Block, + node: Node +) { + block.contextualise(node.expression); + const { dependencies, snippet } = node.metadata; + + // TODO should this be the generator's job? It's duplicated between + // here and the equivalent DOM compiler visitor + const contexts = new Map(block.contexts); + contexts.set(node.value, '__value'); + + const contextDependencies = new Map(block.contextDependencies); + contextDependencies.set(node.value, dependencies); + + const childBlock = block.child({ + contextDependencies, + contexts + }); + + generator.append('${(function(__value) { if(__isPromise(__value)) return `'); + + node.pending.children.forEach((child: Node) => { + visit(generator, childBlock, child); + }); + + generator.append('`; return `'); + + node.then.children.forEach((child: Node) => { + visit(generator, childBlock, child); + }); + + generator.append(`\`;}(${snippet})) }`); +} diff --git a/src/generators/server-side-rendering/visitors/index.ts b/src/generators/server-side-rendering/visitors/index.ts index 0ed4a58e22..468239a949 100644 --- a/src/generators/server-side-rendering/visitors/index.ts +++ b/src/generators/server-side-rendering/visitors/index.ts @@ -1,3 +1,4 @@ +import AwaitBlock from './AwaitBlock'; import Comment from './Comment'; import EachBlock from './EachBlock'; import Element from './Element'; @@ -7,6 +8,7 @@ import RawMustacheTag from './RawMustacheTag'; import Text from './Text'; export default { + AwaitBlock, Comment, EachBlock, Element, diff --git a/src/parse/index.ts b/src/parse/index.ts index c725ab90c3..ff2714cb03 100644 --- a/src/parse/index.ts +++ b/src/parse/index.ts @@ -3,6 +3,7 @@ import fragment from './state/fragment'; import { whitespace } from '../utils/patterns'; import { trimStart, trimEnd } from '../utils/trim'; import getCodeFrame from '../utils/getCodeFrame'; +import reservedNames from '../utils/reservedNames'; import hash from './utils/hash'; import { Node, Parsed } from '../interfaces'; import CompileError from '../utils/CompileError'; @@ -139,6 +140,17 @@ export class Parser { return match[0]; } + readIdentifier() { + const start = this.index; + const identifier = this.read(/[a-zA-Z_$][a-zA-Z0-9_$]*/); + + if (reservedNames.has(identifier)) { + this.error(`'${identifier}' is a reserved word in JavaScript and cannot be used here`, start); + } + + return identifier; + } + readUntil(pattern: RegExp) { if (this.index >= this.template.length) this.error('Unexpected end of input'); diff --git a/src/parse/state/mustache.ts b/src/parse/state/mustache.ts index 3ca6003242..b59cf92b8f 100644 --- a/src/parse/state/mustache.ts +++ b/src/parse/state/mustache.ts @@ -5,9 +5,9 @@ import reservedNames from '../../utils/reservedNames'; import { Parser } from '../index'; import { Node } from '../../interfaces'; -const validIdentifier = /[a-zA-Z_$][a-zA-Z0-9_$]*/; - function trimWhitespace(block: Node, trimBefore: boolean, trimAfter: boolean) { + if (!block.children) return; // AwaitBlock + const firstChild = block.children[0]; const lastChild = block.children[block.children.length - 1]; @@ -41,16 +41,20 @@ export default function mustache(parser: Parser) { let block = parser.current(); let expected; - if (block.type === 'ElseBlock') { + if (block.type === 'ElseBlock' || block.type === 'PendingBlock' || block.type === 'ThenBlock' || block.type === 'CatchBlock') { block.end = start; parser.stack.pop(); block = parser.current(); + + expected = 'await'; } if (block.type === 'IfBlock') { expected = 'if'; } else if (block.type === 'EachBlock') { expected = 'each'; + } else if (block.type === 'AwaitBlock') { + expected = 'await'; } else { parser.error(`Unexpected block closing tag`); } @@ -70,7 +74,7 @@ export default function mustache(parser: Parser) { } // strip leading/trailing whitespace as necessary - if (!block.children.length) parser.error(`Empty block`, block.start); + if (block.children && !block.children.length) parser.error(`Empty block`, block.start); const charBefore = parser.template[block.start - 1]; const charAfter = parser.template[parser.index]; @@ -131,6 +135,53 @@ export default function mustache(parser: Parser) { }; parser.stack.push(block.else); + } else if (parser.eat('then')) { + // TODO DRY out this and the next section + const pendingBlock = parser.current(); + if (pendingBlock.type === 'PendingBlock') { + pendingBlock.end = start; + parser.stack.pop(); + const awaitBlock = parser.current(); + + parser.requireWhitespace(); + awaitBlock.value = parser.readIdentifier(); + + parser.allowWhitespace(); + parser.eat('}}', true); + + const thenBlock: Node = { + start, + end: null, + type: 'ThenBlock', + children: [] + }; + + awaitBlock.then = thenBlock; + parser.stack.push(thenBlock); + } + } else if (parser.eat('catch')) { + const thenBlock = parser.current(); + if (thenBlock.type === 'ThenBlock') { + thenBlock.end = start; + parser.stack.pop(); + const awaitBlock = parser.current(); + + parser.requireWhitespace(); + awaitBlock.error = parser.readIdentifier(); + + parser.allowWhitespace(); + parser.eat('}}', true); + + const catchBlock: Node = { + start, + end: null, + type: 'CatchBlock', + children: [] + }; + + awaitBlock.catch = catchBlock; + parser.stack.push(catchBlock); + } } else if (parser.eat('#')) { // {{#if foo}} or {{#each foo}} let type; @@ -139,21 +190,50 @@ export default function mustache(parser: Parser) { type = 'IfBlock'; } else if (parser.eat('each')) { type = 'EachBlock'; + } else if (parser.eat('await')) { + type = 'AwaitBlock'; } else { - parser.error(`Expected if or each`); + parser.error(`Expected if, each or await`); } parser.requireWhitespace(); const expression = readExpression(parser); - const block: Node = { - start, - end: null, - type, - expression, - children: [], - }; + const block: Node = type === 'AwaitBlock' ? + { + start, + end: null, + type, + expression, + value: null, + error: null, + pending: { + start: null, + end: null, + type: 'PendingBlock', + children: [] + }, + then: { + start: null, + end: null, + type: 'ThenBlock', + children: [] + }, + catch: { + start: null, + end: null, + type: 'CatchBlock', + children: [] + }, + } : + { + start, + end: null, + type, + expression, + children: [], + }; parser.allowWhitespace(); @@ -170,13 +250,8 @@ export default function mustache(parser: Parser) { do { parser.allowWhitespace(); - const start = parser.index; - const destructuredContext = parser.read(validIdentifier); - + const destructuredContext = parser.readIdentifier(); if (!destructuredContext) parser.error(`Expected name`); - if (reservedNames.has(destructuredContext)) { - parser.error(`'${destructuredContext}' is a reserved word in JavaScript and cannot be used here`, start); - } block.destructuredContexts.push(destructuredContext); parser.allowWhitespace(); @@ -188,12 +263,7 @@ export default function mustache(parser: Parser) { parser.allowWhitespace(); parser.eat(']', true); } else { - const start = parser.index; - block.context = parser.read(validIdentifier); - if (reservedNames.has(block.context)) { - parser.error(`'${block.context}' is a reserved word in JavaScript and cannot be used here`, start); - } - + block.context = parser.readIdentifier(); if (!block.context) parser.error(`Expected name`); } @@ -201,13 +271,13 @@ export default function mustache(parser: Parser) { if (parser.eat(',')) { parser.allowWhitespace(); - block.index = parser.read(validIdentifier); + block.index = parser.readIdentifier(); if (!block.index) parser.error(`Expected name`); parser.allowWhitespace(); } if (parser.eat('@')) { - block.key = parser.read(validIdentifier); + block.key = parser.readIdentifier(); if (!block.key) parser.error(`Expected name`); parser.allowWhitespace(); } @@ -217,6 +287,11 @@ export default function mustache(parser: Parser) { parser.current().children.push(block); parser.stack.push(block); + + if (type === 'AwaitBlock') { + block.pending.start = parser.index; + parser.stack.push(block.pending); + } } else if (parser.eat('yield')) { // {{yield}} // TODO deprecate diff --git a/src/shared/index.js b/src/shared/index.js index 9825972b82..b63a4ab001 100644 --- a/src/shared/index.js +++ b/src/shared/index.js @@ -188,6 +188,14 @@ export function _unmount() { this._fragment.u(); } +export function isPromise(value) { + return value && typeof value.then === 'function'; +} + +export var PENDING = {}; +export var SUCCESS = {}; +export var FAILURE = {}; + export function removeFromStore() { this.store._remove(this); } diff --git a/test/js/samples/ssr-no-oncreate-etc/expected.js b/test/js/samples/ssr-no-oncreate-etc/expected.js index c64b3877ab..511e5dd74e 100644 --- a/test/js/samples/ssr-no-oncreate-etc/expected.js +++ b/test/js/samples/ssr-no-oncreate-etc/expected.js @@ -22,16 +22,4 @@ SvelteComponent.renderCss = function() { }; }; -var escaped = { - '"': '"', - "'": ''', - '&': '&', - '<': '<', - '>': '>' -}; - -function __escape(html) { - return String(html).replace(/["'&<>]/g, match => escaped[match]); -} - module.exports = SvelteComponent; \ No newline at end of file diff --git a/test/parser/index.js b/test/parser/index.js index 6bdd1b6376..33e36e98cd 100644 --- a/test/parser/index.js +++ b/test/parser/index.js @@ -41,7 +41,8 @@ describe('parse', () => { assert.deepEqual(err.loc, expected.loc); assert.equal(err.pos, expected.pos); } catch (err2) { - throw err2.code === 'MODULE_NOT_FOUND' ? err : err2; + const e = err2.code === 'MODULE_NOT_FOUND' ? err : err2; + throw e; } } }); diff --git a/test/parser/samples/await-then-catch/input.html b/test/parser/samples/await-then-catch/input.html new file mode 100644 index 0000000000..36489b9043 --- /dev/null +++ b/test/parser/samples/await-then-catch/input.html @@ -0,0 +1,7 @@ +{{#await thePromise}} +

loading...

+{{then theValue}} +

the value is {{theValue}}

+{{catch theError}} +

oh no! {{theError.message}}

+{{/await}} \ No newline at end of file diff --git a/test/parser/samples/await-then-catch/output.json b/test/parser/samples/await-then-catch/output.json new file mode 100644 index 0000000000..4f149e6a0c --- /dev/null +++ b/test/parser/samples/await-then-catch/output.json @@ -0,0 +1,161 @@ +{ + "hash": 1040536517, + "html": { + "start": 0, + "end": 158, + "type": "Fragment", + "children": [ + { + "start": 0, + "end": 158, + "type": "AwaitBlock", + "expression": { + "type": "Identifier", + "start": 9, + "end": 19, + "name": "thePromise" + }, + "value": "theValue", + "error": "theError", + "pending": { + "start": 21, + "end": 41, + "type": "PendingBlock", + "children": [ + { + "start": 21, + "end": 23, + "type": "Text", + "data": "\n\t" + }, + { + "start": 23, + "end": 40, + "type": "Element", + "name": "p", + "attributes": [], + "children": [ + { + "start": 26, + "end": 36, + "type": "Text", + "data": "loading..." + } + ] + }, + { + "start": 40, + "end": 41, + "type": "Text", + "data": "\n" + } + ] + }, + "then": { + "start": 41, + "end": 93, + "type": "ThenBlock", + "children": [ + { + "start": 58, + "end": 60, + "type": "Text", + "data": "\n\t" + }, + { + "start": 60, + "end": 92, + "type": "Element", + "name": "p", + "attributes": [], + "children": [ + { + "start": 63, + "end": 76, + "type": "Text", + "data": "the value is " + }, + { + "start": 76, + "end": 88, + "type": "MustacheTag", + "expression": { + "type": "Identifier", + "start": 78, + "end": 86, + "name": "theValue" + } + } + ] + }, + { + "start": 92, + "end": 93, + "type": "Text", + "data": "\n" + } + ] + }, + "catch": { + "start": 93, + "end": 148, + "type": "CatchBlock", + "children": [ + { + "start": 111, + "end": 113, + "type": "Text", + "data": "\n\t" + }, + { + "start": 113, + "end": 147, + "type": "Element", + "name": "p", + "attributes": [], + "children": [ + { + "start": 116, + "end": 123, + "type": "Text", + "data": "oh no! " + }, + { + "start": 123, + "end": 143, + "type": "MustacheTag", + "expression": { + "type": "MemberExpression", + "start": 125, + "end": 141, + "object": { + "type": "Identifier", + "start": 125, + "end": 133, + "name": "theError" + }, + "property": { + "type": "Identifier", + "start": 134, + "end": 141, + "name": "message" + }, + "computed": false + } + } + ] + }, + { + "start": 147, + "end": 148, + "type": "Text", + "data": "\n" + } + ] + } + } + ] + }, + "css": null, + "js": null +} \ No newline at end of file diff --git a/test/runtime/index.js b/test/runtime/index.js index a82f5b6975..af60caa84c 100644 --- a/test/runtime/index.js +++ b/test/runtime/index.js @@ -89,7 +89,7 @@ describe("runtime", () => { } } catch (err) { failed.add(dir); - showOutput(cwd, { shared, store: !!compileOptions.store }, svelte); // eslint-disable-line no-console + showOutput(cwd, { shared, format: 'cjs', store: !!compileOptions.store }, svelte); // eslint-disable-line no-console throw err; } } @@ -106,96 +106,97 @@ describe("runtime", () => { const window = env(); - try { - // set of hacks to support transition tests - transitionManager.running = false; - transitionManager.transitions = []; - - const raf = { - time: 0, - callback: null, - tick: now => { - raf.time = now; - if (raf.callback) raf.callback(); - } - }; - window.performance = { now: () => raf.time }; - global.requestAnimationFrame = cb => { - let called = false; - raf.callback = () => { - if (!called) { - called = true; - cb(); + return Promise.resolve() + .then(() => { + // set of hacks to support transition tests + transitionManager.running = false; + transitionManager.transitions = []; + + const raf = { + time: 0, + callback: null, + tick: now => { + raf.time = now; + if (raf.callback) raf.callback(); } }; - }; - - global.window = window; - - try { - SvelteComponent = require(`./samples/${dir}/main.html`); - } catch (err) { - showOutput(cwd, { shared, hydratable: hydrate, store: !!compileOptions.store }, svelte); // eslint-disable-line no-console - throw err; - } + window.performance = { now: () => raf.time }; + global.requestAnimationFrame = cb => { + let called = false; + raf.callback = () => { + if (!called) { + called = true; + cb(); + } + }; + }; - global.window = window; + try { + SvelteComponent = require(`./samples/${dir}/main.html`); + } catch (err) { + showOutput(cwd, { shared, format: 'cjs', hydratable: hydrate, store: !!compileOptions.store }, svelte); // eslint-disable-line no-console + throw err; + } - // Put the constructor on window for testing - window.SvelteComponent = SvelteComponent; + global.window = window; - const target = window.document.querySelector("main"); + // Put the constructor on window for testing + window.SvelteComponent = SvelteComponent; - const warnings = []; - const warn = console.warn; - console.warn = warning => { - warnings.push(warning); - }; + const target = window.document.querySelector("main"); - const options = Object.assign({}, { - target, - hydrate, - data: config.data, - store: config.store - }, config.options || {}); + const warnings = []; + const warn = console.warn; + console.warn = warning => { + warnings.push(warning); + }; - const component = new SvelteComponent(options); + const options = Object.assign({}, { + target, + hydrate, + data: config.data, + store: config.store + }, config.options || {}); - console.warn = warn; + const component = new SvelteComponent(options); - if (config.error) { - unintendedError = true; - throw new Error("Expected a runtime error"); - } + console.warn = warn; - if (config.warnings) { - assert.deepEqual(warnings, config.warnings); - } else if (warnings.length) { - unintendedError = true; - throw new Error("Received unexpected warnings"); - } + if (config.error) { + unintendedError = true; + throw new Error("Expected a runtime error"); + } - if (config.html) { - assert.htmlEqual(target.innerHTML, config.html); - } + if (config.warnings) { + assert.deepEqual(warnings, config.warnings); + } else if (warnings.length) { + unintendedError = true; + throw new Error("Received unexpected warnings"); + } - if (config.test) { - config.test(assert, component, target, window, raf); - } else { - component.destroy(); - assert.equal(target.innerHTML, ""); - } - } catch (err) { - if (config.error && !unintendedError) { - config.error(assert, err); - } else { - failed.add(dir); - showOutput(cwd, { shared, hydratable: hydrate, store: !!compileOptions.store }, svelte); // eslint-disable-line no-console - throw err; - } - } + if (config.html) { + assert.htmlEqual(target.innerHTML, config.html); + } - if (config.show) showOutput(cwd, { shared, hydratable: hydrate, store: !!compileOptions.store }, svelte); + if (config.test) { + return config.test(assert, component, target, window, raf); + } else { + component.destroy(); + assert.equal(target.innerHTML, ""); + } + }) + .catch(err => { + if (config.error && !unintendedError) { + config.error(assert, err); + } else { + failed.add(dir); + showOutput(cwd, { shared, format: 'cjs', hydratable: hydrate, store: !!compileOptions.store }, svelte); // eslint-disable-line no-console + throw err; + } + }) + .then(() => { + if (config.show) showOutput(cwd, { shared, format: 'cjs', hydratable: hydrate, store: !!compileOptions.store }, svelte); + }); }); } diff --git a/test/runtime/samples/await-then-catch-multiple/_config.js b/test/runtime/samples/await-then-catch-multiple/_config.js new file mode 100644 index 0000000000..2866fea883 --- /dev/null +++ b/test/runtime/samples/await-then-catch-multiple/_config.js @@ -0,0 +1,53 @@ +let fulfil; + +let thePromise = new Promise(f => { + fulfil = f; +}); + +export default { + data: { + thePromise + }, + + html: ` +

loading...

+

loading...

+ `, + + test(assert, component, target) { + fulfil(42); + + return thePromise + .then(() => { + assert.htmlEqual(target.innerHTML, ` +

the value is 42

+

the value is 42

+ `); + + let reject; + + thePromise = new Promise((f, r) => { + reject = r; + }); + + component.set({ + thePromise + }); + + assert.htmlEqual(target.innerHTML, ` +

loading...

+

loading...

+ `); + + reject(new Error('something broke')); + + return thePromise.catch(() => {}); + }) + .then(() => { + assert.htmlEqual(target.innerHTML, ` +

oh no! something broke

+

oh no! something broke

+ `); + }); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/await-then-catch-multiple/main.html b/test/runtime/samples/await-then-catch-multiple/main.html new file mode 100644 index 0000000000..eb071c27a8 --- /dev/null +++ b/test/runtime/samples/await-then-catch-multiple/main.html @@ -0,0 +1,15 @@ +{{#await thePromise}} +

loading...

+{{then theValue}} +

the value is {{theValue}}

+{{catch theError}} +

oh no! {{theError.message}}

+{{/await}} + +{{#await thePromise}} +

loading...

+{{then theValue}} +

the value is {{theValue}}

+{{catch theError}} +

oh no! {{theError.message}}

+{{/await}} \ No newline at end of file diff --git a/test/runtime/samples/await-then-catch-non-promise/_config.js b/test/runtime/samples/await-then-catch-non-promise/_config.js new file mode 100644 index 0000000000..56b4b59f1b --- /dev/null +++ b/test/runtime/samples/await-then-catch-non-promise/_config.js @@ -0,0 +1,19 @@ +export default { + data: { + thePromise: 'not actually a promise' + }, + + html: ` +

the value is not actually a promise

+ `, + + test(assert, component, target) { + component.set({ + thePromise: 'still not a promise' + }); + + assert.htmlEqual(target.innerHTML, ` +

the value is still not a promise

+ `); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/await-then-catch-non-promise/main.html b/test/runtime/samples/await-then-catch-non-promise/main.html new file mode 100644 index 0000000000..36489b9043 --- /dev/null +++ b/test/runtime/samples/await-then-catch-non-promise/main.html @@ -0,0 +1,7 @@ +{{#await thePromise}} +

loading...

+{{then theValue}} +

the value is {{theValue}}

+{{catch theError}} +

oh no! {{theError.message}}

+{{/await}} \ No newline at end of file diff --git a/test/runtime/samples/await-then-catch/_config.js b/test/runtime/samples/await-then-catch/_config.js new file mode 100644 index 0000000000..ed467b0382 --- /dev/null +++ b/test/runtime/samples/await-then-catch/_config.js @@ -0,0 +1,49 @@ +let fulfil; + +let thePromise = new Promise(f => { + fulfil = f; +}); + +export default { + data: { + thePromise + }, + + html: ` +

loading...

+ `, + + test(assert, component, target) { + fulfil(42); + + return thePromise + .then(() => { + assert.htmlEqual(target.innerHTML, ` +

the value is 42

+ `); + + let reject; + + thePromise = new Promise((f, r) => { + reject = r; + }); + + component.set({ + thePromise + }); + + assert.htmlEqual(target.innerHTML, ` +

loading...

+ `); + + reject(new Error('something broke')); + + return thePromise.catch(() => {}); + }) + .then(() => { + assert.htmlEqual(target.innerHTML, ` +

oh no! something broke

+ `); + }); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/await-then-catch/main.html b/test/runtime/samples/await-then-catch/main.html new file mode 100644 index 0000000000..36489b9043 --- /dev/null +++ b/test/runtime/samples/await-then-catch/main.html @@ -0,0 +1,7 @@ +{{#await thePromise}} +

loading...

+{{then theValue}} +

the value is {{theValue}}

+{{catch theError}} +

oh no! {{theError.message}}

+{{/await}} \ No newline at end of file