- November 15th 2020, 8:11:25 am
+ November 15th 2020, 12:16:39 pm
Last updated
diff --git a/package-lock.json b/package-lock.json
index 96acdf0..1af0e47 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -339,7 +339,6 @@
"version": "2.5.7",
"resolved": "https://registry.npmjs.org/@poppinss/utils/-/utils-2.5.7.tgz",
"integrity": "sha512-O2Qjz+iIRTIJAwwH/bOml9nj4/kszcpoEyNoeir1KhxsyXhstnHNeTK9FsMlJz38JOs1y1h/tAP3qkkEHlTMkg==",
- "dev": true,
"requires": {
"buffer-alloc": "^1.2.0",
"fast-safe-stringify": "^2.0.7",
@@ -1143,7 +1142,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
"integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==",
- "dev": true,
"requires": {
"buffer-alloc-unsafe": "^1.1.0",
"buffer-fill": "^1.0.0"
@@ -1152,14 +1150,12 @@
"buffer-alloc-unsafe": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz",
- "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==",
- "dev": true
+ "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg=="
},
"buffer-fill": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
- "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=",
- "dev": true
+ "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw="
},
"buffer-from": {
"version": "1.1.1",
@@ -2281,8 +2277,7 @@
"escape-goat": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz",
- "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==",
- "dev": true
+ "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q=="
},
"escape-string-regexp": {
"version": "1.0.5",
@@ -2736,8 +2731,7 @@
"fast-safe-stringify": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz",
- "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==",
- "dev": true
+ "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA=="
},
"fastq": {
"version": "1.8.0",
@@ -2931,8 +2925,7 @@
"fs-readdir-recursive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz",
- "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==",
- "dev": true
+ "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA=="
},
"fs.realpath": {
"version": "1.0.0",
@@ -4205,8 +4198,7 @@
"klona": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/klona/-/klona-2.0.4.tgz",
- "integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==",
- "dev": true
+ "integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA=="
},
"latest-version": {
"version": "5.1.0",
@@ -5851,8 +5843,7 @@
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
- "dev": true
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"mute-stream": {
"version": "0.0.8",
@@ -7304,8 +7295,7 @@
"require-all": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/require-all/-/require-all-3.0.0.tgz",
- "integrity": "sha1-Rz1JcEvjEBFc4ST3c4Ox69hnExI=",
- "dev": true
+ "integrity": "sha1-Rz1JcEvjEBFc4ST3c4Ox69hnExI="
},
"requireg": {
"version": "0.2.2",
@@ -7366,8 +7356,7 @@
"resolve-from": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
- "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
- "dev": true
+ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="
},
"resolve-global": {
"version": "1.0.0",
@@ -7922,6 +7911,14 @@
"safe-buffer": "~5.1.0"
}
},
+ "stringify-attributes": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/stringify-attributes/-/stringify-attributes-2.0.0.tgz",
+ "integrity": "sha512-wrVfRV6sCCB6wr3gx8OgKsp/9dSWWbKr8ifLfOxEcd/BBoa8d5pAf4BZb/jQW1JZnoZImjvUdxdo3ikYHZmYiw==",
+ "requires": {
+ "escape-goat": "^2.0.0"
+ }
+ },
"strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
diff --git a/package.json b/package.json
index b0fa2d4..16004d2 100644
--- a/package.json
+++ b/package.json
@@ -72,6 +72,7 @@
},
"dependencies": {
"@poppinss/inspect": "^1.0.1",
+ "@poppinss/utils": "^2.5.7",
"edge-error": "^1.0.5",
"edge-lexer": "^3.2.0",
"edge-parser": "^5.3.0",
@@ -80,6 +81,7 @@
"lodash.merge": "^4.6.2",
"lodash.size": "^4.2.0",
"macroable": "^5.0.3",
+ "stringify-attributes": "^2.0.0",
"truncatise": "0.0.8"
},
"husky": {
diff --git a/src/Component/Props.ts b/src/Component/Props.ts
new file mode 100644
index 0000000..5c5e0bc
--- /dev/null
+++ b/src/Component/Props.ts
@@ -0,0 +1,96 @@
+/*
+ * edge
+ *
+ * (c) Harminder Virk
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+import { EdgeError } from 'edge-error'
+import { lodash } from '@poppinss/utils'
+import stringifyAttributes from 'stringify-attributes'
+import { safeValue } from '../Context'
+
+/**
+ * Class to ease interactions with component props
+ */
+export class Props {
+ constructor(options: {
+ component: string
+ state: any
+ caller: { filename: string; lineNumber: number }
+ }) {
+ this[Symbol.for('options')] = options
+ Object.assign(this, options.state)
+ }
+
+ /**
+ * Find if a key exists inside the props
+ */
+ public has(key: string) {
+ const value = lodash.get(this[Symbol.for('options')].state, key)
+ return value !== undefined && value !== null
+ }
+
+ /**
+ * Returns the value of a key from the props. An exception is raised
+ * if value is undefined or null.
+ */
+ public get(key: string, defaultValue?: any) {
+ const value = lodash.get(this[Symbol.for('options')].state, key, defaultValue)
+ if (value === undefined || value === null) {
+ throw new EdgeError(
+ `"${key}" prop is required in order to render the "${
+ this[Symbol.for('options')].component
+ }" component`,
+ 'E_MISSING_PROP',
+ {
+ filename: this[Symbol.for('options')].caller.filename,
+ line: this[Symbol.for('options')].caller.lineNumber,
+ col: 0,
+ }
+ )
+ }
+
+ return value
+ }
+
+ /**
+ * Return only given keys
+ */
+ public only(keys: string[]) {
+ return lodash.pick(this[Symbol.for('options')].state, keys)
+ }
+
+ /**
+ * Return except the mentioned keys
+ */
+ public except(keys: string[]) {
+ return lodash.omit(this[Symbol.for('options')].state, keys)
+ }
+
+ /**
+ * Serializes props to attributes
+ */
+ public serialize(mergeProps?: any) {
+ const props = this[Symbol.for('options')].state
+ return safeValue(stringifyAttributes(lodash.merge({}, props, mergeProps)))
+ }
+
+ /**
+ * Serializes only the given props
+ */
+ public serializeOnly(keys: string[], mergeProps?: any) {
+ const props = this.only(keys)
+ return safeValue(stringifyAttributes(lodash.merge({}, props, mergeProps)))
+ }
+
+ /**
+ * Serialize all props except the given keys
+ */
+ public serializeExcept(keys: string[], mergeProps?: any) {
+ const props = this.except(keys)
+ return safeValue(stringifyAttributes(lodash.merge({}, props, mergeProps)))
+ }
+}
diff --git a/src/Component/Slots.ts b/src/Component/Slots.ts
new file mode 100644
index 0000000..2e4341c
--- /dev/null
+++ b/src/Component/Slots.ts
@@ -0,0 +1,68 @@
+/*
+ * edge
+ *
+ * (c) Harminder Virk
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+import { EdgeError } from 'edge-error'
+import { safeValue } from '../Context'
+import { lodash } from '@poppinss/utils'
+
+/**
+ * Class to ease interactions with component slots
+ */
+export class Slots {
+ constructor(options: {
+ component: string
+ slots: { [name: string]: (...args: any[]) => string }
+ caller: { filename: string; lineNumber: number }
+ }) {
+ this[Symbol.for('options')] = options
+ Object.assign(this, options.slots)
+ }
+
+ /**
+ * Find if a key slot exists or not
+ */
+ public has(key: string) {
+ const value = lodash.get(this[Symbol.for('options')].slots, key)
+ return value !== undefined && value !== null
+ }
+
+ /**
+ * Gives the return value of the slot by calling its function
+ */
+ public render(name: string, ...args: any[]) {
+ const slotFn = lodash.get(this[Symbol.for('options')].slots, name)
+
+ if (typeof slotFn !== 'function') {
+ throw new EdgeError(
+ `"${name}" slot is required in order to render the "${
+ this[Symbol.for('options')].component
+ }" component`,
+ 'E_MISSING_SLOT',
+ {
+ filename: this[Symbol.for('options')].caller.filename,
+ line: this[Symbol.for('options')].caller.lineNumber,
+ col: 0,
+ }
+ )
+ }
+
+ return safeValue(slotFn(...args))
+ }
+
+ /**
+ * Render the slot if it exists
+ */
+ public renderIfExists(name: string, ...args: any[]) {
+ const slotFn = lodash.get(this[Symbol.for('options')].slots, name)
+ if (typeof slotFn !== 'function') {
+ return ''
+ }
+ return safeValue(slotFn(...args))
+ }
+}
diff --git a/src/Contracts/index.ts b/src/Contracts/index.ts
index 2b8bdcc..ba24d91 100644
--- a/src/Contracts/index.ts
+++ b/src/Contracts/index.ts
@@ -112,7 +112,7 @@ export interface CompilerContract {
*/
export interface TemplateContract {
renderInline(templatePath: string, ...localVariables: string[]): Function
- renderWithState(template: string, state: any, slots: any): string
+ renderWithState(template: string, state: any, slots: any, caller: any): string
render(template: string, state: any): string
}
diff --git a/src/Tags/Component.ts b/src/Tags/Component.ts
index b8691f6..558f32b 100644
--- a/src/Tags/Component.ts
+++ b/src/Tags/Component.ts
@@ -312,11 +312,15 @@ export const componentTag: TagContract = {
}
})
+ const caller = new StringifiedObject()
+ caller.add('filename', '$filename')
+ caller.add('lineNumber', '$lineNumber')
+
/**
* Write the line to render the component with it's own state
*/
buffer.outputExpression(
- `template.renderWithState(${name}, ${props}, ${obj.flush()})`,
+ `template.renderWithState(${name}, ${props}, ${obj.flush()}, ${caller.flush()})`,
token.filename,
token.loc.start.line,
false
diff --git a/src/Template/index.ts b/src/Template/index.ts
index e010726..af689d4 100644
--- a/src/Template/index.ts
+++ b/src/Template/index.ts
@@ -10,6 +10,8 @@
import merge from 'lodash.merge'
import { Context } from '../Context'
import { Processor } from '../Processor'
+import { Props } from '../Component/Props'
+import { Slots } from '../Component/Slots'
import { CompilerContract, TemplateContract } from '../Contracts'
/**
@@ -74,10 +76,14 @@ export class Template implements TemplateContract {
* template.renderWithState('components.user', { username: 'virk' }, slotsIfAny)
* ```
*/
- public renderWithState(template: string, state: any, slots: any): string {
+ public renderWithState(template: string, state: any, slots: any, caller: any): string {
const { template: compiledTemplate } = this.compiler.compile(template)
- const templateState = Object.assign({}, this.sharedState, state, { $slots: slots })
+ const templateState = Object.assign({}, this.sharedState, state, {
+ $slots: new Slots({ component: template, caller, slots }),
+ $caller: caller,
+ $props: new Props({ component: template, caller, state }),
+ })
const context = new Context()
return this.wrapToFunction(compiledTemplate)(this, templateState, context)
diff --git a/test/component.spec.ts b/test/component.spec.ts
index 0ff16b9..05c1620 100644
--- a/test/component.spec.ts
+++ b/test/component.spec.ts
@@ -344,4 +344,32 @@ test.group('Component | render | errors', (group) => {
assert.equal(error.filename, join(fs.basePath, 'button.edge'))
}
})
+
+ test('point error back to the caller when props validation fails', async (assert) => {
+ assert.plan(4)
+
+ await fs.add('button.edge', `{{ $props.get('text') }}`)
+
+ await fs.add(
+ 'eval.edge',
+ dedent`
+ Some content
+
+ @!component('button')
+ `
+ )
+
+ const template = new Template(compiler, {}, {}, processor)
+ try {
+ template.render('eval.edge', {})
+ } catch (error) {
+ assert.equal(
+ error.message,
+ '"text" prop is required in order to render the "button" component'
+ )
+ assert.equal(error.line, 3)
+ assert.equal(error.col, 0)
+ assert.equal(error.filename, join(fs.basePath, 'eval.edge'))
+ }
+ })
})
diff --git a/test/props.spec.ts b/test/props.spec.ts
new file mode 100644
index 0000000..fc1d193
--- /dev/null
+++ b/test/props.spec.ts
@@ -0,0 +1,243 @@
+/*
+ * edge
+ *
+ * (c) Harminder Virk
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+import test from 'japa'
+import { Props } from '../src/Component/Props'
+
+test.group('Props', () => {
+ test('get props value', (assert) => {
+ const props = new Props({
+ component: 'foo',
+ state: { title: 'Hello' },
+ caller: {
+ filename: 'bar.edge',
+ lineNumber: 1,
+ },
+ })
+
+ assert.equal(props.get('title'), 'Hello')
+ })
+
+ test('find if props has value', (assert) => {
+ const props = new Props({
+ component: 'foo',
+ state: { title: 'Hello' },
+ caller: {
+ filename: 'bar.edge',
+ lineNumber: 1,
+ },
+ })
+
+ assert.isTrue(props.has('title'))
+ })
+
+ test('raise error when prop value is missing', (assert) => {
+ assert.plan(3)
+
+ const props = new Props({
+ component: 'foo',
+ state: { title: 'Hello' },
+ caller: {
+ filename: 'bar.edge',
+ lineNumber: 1,
+ },
+ })
+
+ try {
+ props.get('age')
+ } catch (error) {
+ assert.equal(error.message, '"age" prop is required in order to render the "foo" component')
+ assert.equal(error.filename, 'bar.edge')
+ assert.equal(error.line, 1)
+ }
+ })
+
+ test('cherry pick values from the props', (assert) => {
+ const props = new Props({
+ component: 'foo',
+ state: { title: 'Hello', label: 'Hello world', actionText: 'Confirm' },
+ caller: {
+ filename: 'bar.edge',
+ lineNumber: 1,
+ },
+ })
+
+ assert.deepEqual(props.only(['label', 'actionText']), {
+ label: 'Hello world',
+ actionText: 'Confirm',
+ })
+ })
+
+ test('get values except for the defined keys from the props', (assert) => {
+ const props = new Props({
+ component: 'foo',
+ state: { title: 'Hello', label: 'Hello world', actionText: 'Confirm' },
+ caller: {
+ filename: 'bar.edge',
+ lineNumber: 1,
+ },
+ })
+
+ assert.deepEqual(props.except(['label', 'actionText']), {
+ title: 'Hello',
+ })
+ })
+
+ test('serialize props to html attributes', (assert) => {
+ const props = new Props({
+ component: 'foo',
+ state: {
+ class: ['foo', 'bar'],
+ onclick: 'foo = bar',
+ },
+ caller: {
+ filename: 'bar.edge',
+ lineNumber: 1,
+ },
+ })
+
+ assert.equal(props.serialize().value, ' class="foo bar" onclick="foo = bar"')
+ })
+
+ test('serialize by merging custom properties', (assert) => {
+ const props = new Props({
+ component: 'foo',
+ state: {
+ class: ['foo', 'bar'],
+ onclick: 'foo = bar',
+ },
+ caller: {
+ filename: 'bar.edge',
+ lineNumber: 1,
+ },
+ })
+
+ assert.equal(props.serialize({ id: '1' }).value, ' class="foo bar" onclick="foo = bar" id="1"')
+ })
+
+ test('serialize specific keys to html attributes', (assert) => {
+ const props = new Props({
+ component: 'foo',
+ state: {
+ class: ['foo', 'bar'],
+ onclick: 'foo = bar',
+ },
+ caller: {
+ filename: 'bar.edge',
+ lineNumber: 1,
+ },
+ })
+
+ assert.equal(props.serializeOnly(['class']).value, ' class="foo bar"')
+ })
+
+ test('serialize specific keys to merge custom properties', (assert) => {
+ const props = new Props({
+ component: 'foo',
+ state: {
+ class: ['foo', 'bar'],
+ onclick: 'foo = bar',
+ },
+ caller: {
+ filename: 'bar.edge',
+ lineNumber: 1,
+ },
+ })
+
+ assert.equal(props.serializeOnly(['class'], { id: '1' }).value, ' class="foo bar" id="1"')
+ })
+
+ test('serialize all except defined keys to html attributes', (assert) => {
+ const props = new Props({
+ component: 'foo',
+ state: {
+ class: ['foo', 'bar'],
+ onclick: 'foo = bar',
+ },
+ caller: {
+ filename: 'bar.edge',
+ lineNumber: 1,
+ },
+ })
+
+ assert.equal(props.serializeExcept(['class']).value, ' onclick="foo = bar"')
+ })
+
+ test('serialize specific keys to merge custom properties', (assert) => {
+ const props = new Props({
+ component: 'foo',
+ state: {
+ class: ['foo', 'bar'],
+ onclick: 'foo = bar',
+ },
+ caller: {
+ filename: 'bar.edge',
+ lineNumber: 1,
+ },
+ })
+
+ assert.equal(props.serializeExcept(['class'], { id: '1' }).value, ' onclick="foo = bar" id="1"')
+ })
+
+ test('copy state properties to the props class', (assert) => {
+ const props = new Props({
+ component: 'foo',
+ state: {
+ class: ['foo', 'bar'],
+ onclick: 'foo = bar',
+ },
+ caller: {
+ filename: 'bar.edge',
+ lineNumber: 1,
+ },
+ })
+
+ assert.deepEqual(props['class'], ['foo', 'bar'])
+ assert.deepEqual(props['onclick'], 'foo = bar')
+ })
+
+ test('access nested state properties from the props instance', (assert) => {
+ const props = new Props({
+ component: 'foo',
+ state: {
+ user: {
+ name: 'virk',
+ },
+ },
+ caller: {
+ filename: 'bar.edge',
+ lineNumber: 1,
+ },
+ })
+
+ assert.equal(props['user']['name'], 'virk')
+ })
+
+ test('do not raise error when state is undefined', () => {
+ new Props({
+ component: 'foo',
+ state: undefined,
+ caller: {
+ filename: 'bar.edge',
+ lineNumber: 1,
+ },
+ })
+ })
+
+ test('do not raise error when state is null', () => {
+ new Props({
+ component: 'foo',
+ state: null,
+ caller: {
+ filename: 'bar.edge',
+ lineNumber: 1,
+ },
+ })
+ })
+})
diff --git a/test/slots.spec.ts b/test/slots.spec.ts
new file mode 100644
index 0000000..ba59044
--- /dev/null
+++ b/test/slots.spec.ts
@@ -0,0 +1,75 @@
+/*
+ * edge
+ *
+ * (c) Harminder Virk
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+import test from 'japa'
+import { Slots } from '../src/Component/Slots'
+
+test.group('Slots', () => {
+ test('render slot', (assert) => {
+ const slots = new Slots({
+ component: 'foo',
+ slots: {
+ main: () => 'hello',
+ },
+ caller: {
+ filename: 'bar.edge',
+ lineNumber: 1,
+ },
+ })
+
+ assert.equal(slots.render('main').value, 'hello')
+ })
+
+ test('raise error when slot is missing', (assert) => {
+ const slots = new Slots({
+ component: 'foo',
+ slots: {},
+ caller: {
+ filename: 'bar.edge',
+ lineNumber: 1,
+ },
+ })
+
+ try {
+ slots.render('main')
+ } catch (error) {
+ assert.equal(error.message, '"main" slot is required in order to render the "foo" component')
+ assert.equal(error.filename, 'bar.edge')
+ assert.equal(error.line, 1)
+ }
+ })
+
+ test('return empty string when slot is missing', (assert) => {
+ const slots = new Slots({
+ component: 'foo',
+ slots: {},
+ caller: {
+ filename: 'bar.edge',
+ lineNumber: 1,
+ },
+ })
+
+ assert.equal(slots.renderIfExists('main'), '')
+ })
+
+ test('access slots directly', (assert) => {
+ const slots = new Slots({
+ component: 'foo',
+ slots: {
+ main: () => 'hello',
+ },
+ caller: {
+ filename: 'bar.edge',
+ lineNumber: 1,
+ },
+ })
+
+ assert.equal(slots['main'](), 'hello')
+ })
+})