From d27088fa6824377823d8c160097fe0edefa89f98 Mon Sep 17 00:00:00 2001 From: Jerry Nummi Date: Sun, 10 Feb 2019 11:46:51 -0800 Subject: [PATCH] WIP: remove view tree --- app/components/view-item.js | 85 ------ app/controllers/view-tree.js | 79 ------ app/router.js | 1 - app/routes/component-tree.js | 39 ++- app/routes/view-tree.js | 57 ---- app/schemas/view-tree.js | 30 -- app/templates/components/side-nav.hbs | 6 - app/templates/components/view-item.hbs | 65 ----- app/templates/nav.hbs | 6 - app/templates/view-tree-toolbar.hbs | 31 --- app/templates/view-tree.hbs | 26 -- public/assets/svg/nav-view-tree.svg | 6 - tests/acceptance/view-tree-test.js | 362 ------------------------- 13 files changed, 37 insertions(+), 756 deletions(-) delete mode 100644 app/components/view-item.js delete mode 100644 app/controllers/view-tree.js delete mode 100644 app/routes/view-tree.js delete mode 100644 app/schemas/view-tree.js delete mode 100644 app/templates/components/view-item.hbs delete mode 100644 app/templates/view-tree-toolbar.hbs delete mode 100644 app/templates/view-tree.hbs delete mode 100644 public/assets/svg/nav-view-tree.svg delete mode 100644 tests/acceptance/view-tree-test.js diff --git a/app/components/view-item.js b/app/components/view-item.js deleted file mode 100644 index 270b7e5539..0000000000 --- a/app/components/view-item.js +++ /dev/null @@ -1,85 +0,0 @@ -import { computed } from '@ember/object'; -import Component from '@ember/component'; -import { htmlSafe } from '@ember/string'; -import { not, bool, equal } from '@ember/object/computed'; - -export default Component.extend({ - /** - * No tag. This component should not affect - * the DOM. - * - * @property tagName - * @type {String} - * @default '' - */ - tagName: '', - - /** - * Has a view (component) instance. - * - * @property hasView - * @type {Boolean} - */ - hasView: bool('model.value.viewClass'), - - /** - * Whether it has a tag or not. - * - * @property isTagless - * @type {Boolean} - */ - isTagless: equal('model.value.tagName', ''), - - /** - * Whether it has an element or not (depends on the tagName). - * - * @property hasElement - * @type {Boolean} - */ - hasElement: not('isTagless'), - - /** - * Whether it has a layout/template or not. - * - * @property hasTemplate - * @type {Boolean} - */ - hasTemplate: bool('model.value.template'), - - hasModel: bool('model.value.model'), - - hasController: bool('model.value.controller'), - - modelInspectable: computed('hasModel', 'model.value.model.type', function() { - return this.get('hasModel') && this.get('model.value.model.type') === 'type-ember-object'; - }), - - labelStyle: computed('model.parentCount', function() { - return htmlSafe(`padding-left: ${+this.get('model.parentCount') * 20 + 5}px;`); - }), - - actions: { - inspectView() { - if (this.get('hasView')) { - this.inspect(this.get('model.value.objectId')); - } - }, - inspectElement(objectId) { - let elementId; - if (!objectId && this.get('hasElement')) { - objectId = this.get('model.value.objectId'); - } - if (!objectId) { - elementId = this.get('model.value.elementId'); - } - if (objectId || elementId) { - this.inspectElement({ objectId, elementId }); - } - }, - inspectModel(objectId) { - if (this.get('modelInspectable')) { - this.inspect(objectId); - } - } - } -}); diff --git a/app/controllers/view-tree.js b/app/controllers/view-tree.js deleted file mode 100644 index bceb9263d1..0000000000 --- a/app/controllers/view-tree.js +++ /dev/null @@ -1,79 +0,0 @@ -import { observer, get, computed } from '@ember/object'; -import Controller, { inject as controller } from '@ember/controller'; -import searchMatch from 'ember-inspector/utils/search-match'; -import { alias } from '@ember/object/computed'; - -export default Controller.extend({ - application: controller(), - pinnedObjectId: null, - inspectingViews: false, - queryParams: ['components'], - components: alias('options.components'), - - /** - * Bound to the search field to filter the component list. - * - * @property searchValue - * @type {String} - * @default '' - */ - searchValue: '', - - /** - * The filtered view list. - * - * @property filteredList - * @type {Array} - */ - filteredList: computed('model.[]', 'searchValue', function() { - return get(this, 'model') - .filter((item) => searchMatch(get(item, 'value.name'), this.get('searchValue'))); - }), - - optionsChanged: observer('options.components', function() { - this.port.send('view:setOptions', { options: this.get('options') }); - }), - - init() { - this._super(...arguments); - - this.options = { - components: false - }; - }, - - actions: { - hidePreview() { - this.get('port').send('view:hidePreview'); - }, - - inspect(objectId) { - if (objectId) { - this.get('port').send('objectInspector:inspectById', { objectId }); - } - }, - - inspectElement({ objectId, elementId }) { - this.get('port').send('view:inspectElement', { objectId, elementId }); - }, - - previewLayer({ value: { objectId, elementId, renderNodeId } }) { - // We are passing all of objectId, elementId, and renderNodeId to support post-glimmer 1, post-glimmer 2, and root for - // post-glimmer 2 - this.get('port').send('view:previewLayer', { objectId, renderNodeId, elementId }); - }, - - sendModelToConsole(value) { - // do not use `sendObjectToConsole` because models don't have to be ember objects - this.get('port').send('view:sendModelToConsole', value); - }, - - sendObjectToConsole(objectId) { - this.get('port').send('objectInspector:sendToConsole', { objectId }); - }, - - toggleViewInspection() { - this.get('port').send('view:inspectViews', { inspect: !this.get('inspectingViews') }); - } - } -}); diff --git a/app/router.js b/app/router.js index eb2894c15a..85310188a3 100644 --- a/app/router.js +++ b/app/router.js @@ -9,7 +9,6 @@ const Router = EmberRouter.extend({ Router.map(function() { this.route('app-detected', { path: '/', resetNamespace: true }, function() { this.route('launch', { path: '/', resetNamespace: true }); - this.route('view-tree', { resetNamespace: true }); this.route('component-tree', { resetNamespace: true }); this.route('route-tree', { resetNamespace: true }); diff --git a/app/routes/component-tree.js b/app/routes/component-tree.js index 21b1415a5d..2df4a9cdb8 100644 --- a/app/routes/component-tree.js +++ b/app/routes/component-tree.js @@ -1,12 +1,35 @@ -import ViewTree from "ember-inspector/routes/view-tree"; +import TabRoute from "ember-inspector/routes/tab"; -export default ViewTree.extend({ +export default TabRoute.extend({ queryParams: { pinnedObjectId: { replace: true } }, + model() { + return []; + }, + + setupController() { + this._super(...arguments); + this.get('port').on('view:viewTree', this, this.setViewTree); + this.get('port').on('view:stopInspecting', this, this.stopInspecting); + this.get('port').on('view:startInspecting', this, this.startInspecting); + this.get('port').on('view:inspectDOMElement', this, this.inspectDOMElement); + + this.set('controller.viewTreeLoaded', false); + this.get('port').send('view:setOptions', { options: this.get('controller.options') }); + this.get('port').send('view:getTree'); + }, + + deactivate() { + this.get('port').off('view:viewTree', this, this.setViewTree); + this.get('port').off('view:stopInspecting', this, this.stopInspecting); + this.get('port').off('view:startInspecting', this, this.startInspecting); + this.get('port').off('view:inspectDOMElement', this, this.inspectDOMElement); + }, + setViewTree(options) { this.set('controller.viewTree', options.tree); this.set('controller.viewTreeLoaded', true); @@ -26,6 +49,18 @@ export default ViewTree.extend({ this.get('controller').send('inspect', viewId); }, + startInspecting() { + this.set('controller.inspectingViews', true); + }, + + stopInspecting() { + this.set('controller.inspectingViews', false); + }, + + inspectDOMElement({ elementSelector }) { + this.get('port.adapter').inspectDOMElement(elementSelector); + }, + actions: { queryParamsDidChange(params) { const { pinnedObjectId } = params; diff --git a/app/routes/view-tree.js b/app/routes/view-tree.js deleted file mode 100644 index 8825870e3e..0000000000 --- a/app/routes/view-tree.js +++ /dev/null @@ -1,57 +0,0 @@ -import { assign } from '@ember/polyfills'; -import TabRoute from "ember-inspector/routes/tab"; - -export default TabRoute.extend({ - model() { - return []; - }, - - setupController() { - this._super(...arguments); - this.get('port').on('view:viewTree', this, this.setViewTree); - this.get('port').on('view:stopInspecting', this, this.stopInspecting); - this.get('port').on('view:startInspecting', this, this.startInspecting); - this.get('port').on('view:inspectDOMElement', this, this.inspectDOMElement); - - this.set('controller.viewTreeLoaded', false); - this.get('port').send('view:setOptions', { options: this.get('controller.options') }); - this.get('port').send('view:getTree'); - }, - - deactivate() { - this.get('port').off('view:viewTree', this, this.setViewTree); - this.get('port').off('view:stopInspecting', this, this.stopInspecting); - this.get('port').off('view:startInspecting', this, this.startInspecting); - this.get('port').off('view:inspectDOMElement', this, this.inspectDOMElement); - }, - - setViewTree(options) { - let viewArray = topSort(options.tree); - this.set('controller.model', viewArray); - }, - - startInspecting() { - this.set('controller.inspectingViews', true); - }, - - stopInspecting() { - this.set('controller.inspectingViews', false); - }, - - inspectDOMElement({ elementSelector }) { - this.get('port.adapter').inspectDOMElement(elementSelector); - } -}); - -function topSort(tree, list) { - list = list || []; - let view = assign({}, tree); - view.parentCount = view.parentCount || 0; - delete view.children; - list.push(view); - tree.children.forEach(child => { - child.parentCount = view.parentCount + 1; - topSort(child, list); - }); - return list; -} diff --git a/app/schemas/view-tree.js b/app/schemas/view-tree.js deleted file mode 100644 index 57d72129a4..0000000000 --- a/app/schemas/view-tree.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * View tree schema. - */ -export default { - columns: [{ - id: 'name', - name: 'Name', - visible: true - }, { - id: 'template', - name: 'Template', - visible: true - }, { - id: 'model', - name: 'Model', - visible: true - }, { - id: 'controller', - name: 'Controller', - visible: true - }, { - id: 'component', - name: 'View / Component', - visible: true - }, { - id: 'duration', - name: 'Duration', - visible: true - }] -}; diff --git a/app/templates/components/side-nav.hbs b/app/templates/components/side-nav.hbs index d65b0d4a71..0b2fb3c216 100644 --- a/app/templates/components/side-nav.hbs +++ b/app/templates/components/side-nav.hbs @@ -6,12 +6,6 @@ Components {{/link-to}} -
  • - {{#link-to "view-tree" class="nav__item"}} - {{svg-jar "nav-view-tree" width="20px" height="20px" class="nav__item-icon"}} - View Tree - {{/link-to}} -
  • {{#link-to "route-tree" class="nav__item"}} {{svg-jar "nav-route-tree" width="20px" height="20px" class="nav__item-icon"}} diff --git a/app/templates/components/view-item.hbs b/app/templates/components/view-item.hbs deleted file mode 100644 index ed1a2a6be1..0000000000 --- a/app/templates/components/view-item.hbs +++ /dev/null @@ -1,65 +0,0 @@ -{{#list.cell class="list__cell_main"}} -
    - {{model.value.name}} -
    -{{/list.cell}} -{{#list.cell - class="js-view-template" - clickable=hasElement - on-click=(action "inspectElement") -}} - {{if hasTemplate model.value.template "--"}} -{{/list.cell}} - -{{#list.cell class="js-view-model"}} - {{#if hasModel}} -
    - {{model.value.model.name}} -
    -
    - {{ui/send-to-console action=sendModelToConsole param=model.value}} -
    - {{else}} - -- - {{/if}} -{{/list.cell}} - -{{#list.cell class="js-view-controller"}} - {{#if hasController}} -
    - {{model.value.controller.name}} -
    -
    - {{ui/send-to-console - action=sendObjectToConsole - param=model.value.controller.objectId - }} -
    - {{/if}} -{{/list.cell}} - -{{#list.cell class="js-view-class"}} - {{#if hasView}} -
    - {{model.value.viewClass}} -
    -
    - {{ui/send-to-console action=sendObjectToConsole param=model.value.objectId}} -
    - {{else}} - -- - {{/if}} -{{/list.cell}} - -{{#list.cell class="list__cell_size_small list__cell_value_numeric"}} - {{ms-to-time model.value.duration}} -{{/list.cell}} diff --git a/app/templates/nav.hbs b/app/templates/nav.hbs index 9e4a15787a..ce4045d564 100644 --- a/app/templates/nav.hbs +++ b/app/templates/nav.hbs @@ -6,12 +6,6 @@ Components {{/link-to}}
  • -
  • - {{#link-to "view-tree" class="nav__item"}} - {{svg-jar "nav-view-tree" width="20px" height="20px" class="nav__item-icon"}} - View Tree - {{/link-to}} -
  • {{#link-to "route-tree" class="nav__item"}} {{svg-jar "nav-route-tree" width="20px" height="20px" class="nav__item-icon"}} diff --git a/app/templates/view-tree-toolbar.hbs b/app/templates/view-tree-toolbar.hbs deleted file mode 100644 index bb23880884..0000000000 --- a/app/templates/view-tree-toolbar.hbs +++ /dev/null @@ -1,31 +0,0 @@ -
    - - - - -
    - -
    - -
    - - -
    diff --git a/app/templates/view-tree.hbs b/app/templates/view-tree.hbs deleted file mode 100644 index febebb2722..0000000000 --- a/app/templates/view-tree.hbs +++ /dev/null @@ -1,26 +0,0 @@ -{{#x-list - name="view-tree" - schema=(schema-for "view-tree") - setIsDragging=(action "setIsDragging" target=application) -as |list| -}} - {{#list.vertical-collection - filteredList - as |content index| - }} - - {{view-item - model=content - inspect=(action "inspect") - inspectElement=(action "inspectElement") - sendModelToConsole=(action "sendModelToConsole") - sendObjectToConsole=(action "sendObjectToConsole") - list=list - }} - - {{/list.vertical-collection}} -{{/x-list}} diff --git a/public/assets/svg/nav-view-tree.svg b/public/assets/svg/nav-view-tree.svg deleted file mode 100644 index ecc9db4fd8..0000000000 --- a/public/assets/svg/nav-view-tree.svg +++ /dev/null @@ -1,6 +0,0 @@ - \ No newline at end of file diff --git a/tests/acceptance/view-tree-test.js b/tests/acceptance/view-tree-test.js deleted file mode 100644 index 3d41c4efd4..0000000000 --- a/tests/acceptance/view-tree-test.js +++ /dev/null @@ -1,362 +0,0 @@ -import { visit, fillIn, find, findAll, click, triggerEvent } from '@ember/test-helpers'; -import { run } from '@ember/runloop'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import wait from 'ember-test-helpers/wait'; - -let port; - -module('View Tree Tab', function(hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function() { - port = this.owner.lookup('port:main'); - }); - - function textFor(selector, context) { - return context.querySelector(selector).textContent.trim(); - } - - let treeId = 0; - function viewNodeFactory(props) { - if (!props.template) { - props.template = props.name; - } - let obj = { - value: props, - children: [], - treeId: ++treeId - }; - return obj; - } - - function viewTreeFactory(tree) { - let children = tree.children; - delete tree.children; - let viewNode = viewNodeFactory(tree); - if (children) { - for (let i = 0; i < children.length; i++) { - viewNode.children.push(viewTreeFactory(children[i])); - } - } - return viewNode; - } - - function defaultViewTree() { - return viewTreeFactory({ - name: 'application', - isVirtual: false, - isComponent: false, - objectId: 'applicationView', - viewClass: 'App.ApplicationView', - completeViewClass: 'App.ApplicationView', - duration: 10, - controller: { - name: 'App.ApplicationController', - completeName: 'App.ApplicationController', - objectId: 'applicationController' - }, - children: [ - { - name: 'posts', - isVirtual: false, - isComponent: false, - viewClass: 'App.PostsView', - completeViewClass: 'App.PostsView', - duration: 1, - objectId: 'postsView', - model: { - name: 'PostsArray', - completeName: 'PostsArray', - objectId: 'postsArray', - type: 'type-ember-object' - }, - controller: { - name: 'App.PostsController', - completeName: 'App.PostsController', - objectId: 'postsController' - }, - children: [] - }, - { - name: 'comments', - isVirtual: false, - isComponent: false, - viewClass: 'App.CommentsView', - completeViewClass: 'App.CommentsView', - duration: 2.5, - objectId: 'commentsView', - model: { - name: 'CommentsArray', - completeName: 'CommentsArray', - objectId: 'commentsArray', - type: 'type-ember-object' - }, - controller: { - name: 'App.CommentsController', - completeName: 'App.CommentsController', - objectId: 'commentsController' - }, - children: [] - } - ] - }); - } - - test("It should correctly display the view tree", async function(assert) { - let viewTree = defaultViewTree(); - - await visit('/view-tree'); - run(() => { - port.trigger('view:viewTree', { tree: viewTree }); - }); - await wait(); - - let treeNodes = findAll('.js-view-tree-item'); - assert.equal(treeNodes.length, 3, 'expected some tree nodes'); - - let controllerNames = []; - let templateNames = []; - let modelNames = []; - let viewClassNames = []; - let durations = []; - - [...treeNodes].forEach(function(node) { - templateNames.push(textFor('.js-view-template', node)); - controllerNames.push(textFor('.js-view-controller', node)); - viewClassNames.push(textFor('.js-view-class', node)); - modelNames.push(textFor('.js-view-model', node)); - durations.push(textFor('.js-view-duration', node)); - }); - - assert.deepEqual(controllerNames, [ - 'App.ApplicationController', - 'App.PostsController', - 'App.CommentsController' - ], 'expected controller names'); - - assert.deepEqual(templateNames, [ - 'application', - 'posts', - 'comments' - ], 'expected template names'); - - assert.deepEqual(modelNames, [ - '--', - 'PostsArray', - 'CommentsArray' - ], 'expected model names'); - - assert.deepEqual(viewClassNames, [ - 'App.ApplicationView', - 'App.PostsView', - 'App.CommentsView' - ], 'expected view class names'); - - assert.deepEqual(durations, [ - '10.00ms', - '1.00ms', - '2.50ms' - ], 'expected render durations'); - - let titleTips = [...findAll('span[title]')].map(node => node.getAttribute('title')).sort(); - - assert.deepEqual(titleTips, [ - 'App.ApplicationController', - 'App.ApplicationView', - 'App.CommentsController', - 'App.CommentsView', - 'App.PostsController', - 'App.PostsView', - 'CommentsArray', - 'PostsArray', - 'application', - 'application', - 'comments', - 'comments', - 'posts', - 'posts' - ], 'expected title tips'); - }); - - test("It should filter the view tree using the search text", async function(assert) { - let viewTree = defaultViewTree(); - - await visit('/view-tree'); - run(() => { - port.trigger('view:viewTree', { tree: viewTree }); - }); - await wait(); - - let treeNodes = findAll('.js-view-tree-item'); - assert.equal(treeNodes.length, 3, 'expected some tree nodes'); - - await fillIn('.js-filter-views input', 'post'); - treeNodes = findAll('.js-view-tree-item'); - assert.equal(treeNodes.length, 1, 'expected filtered tree nodes'); - - let controllerNames = []; - let templateNames = []; - let modelNames = []; - let viewClassNames = []; - let durations = []; - - [...treeNodes].forEach(function(node) { - templateNames.push(textFor('.js-view-template', node)); - controllerNames.push(textFor('.js-view-controller', node)); - viewClassNames.push(textFor('.js-view-class', node)); - modelNames.push(textFor('.js-view-model', node)); - durations.push(textFor('.js-view-duration', node)); - }); - - assert.deepEqual(controllerNames, [ - 'App.PostsController', - ], 'expected controller names'); - - assert.deepEqual(templateNames, [ - 'posts', - ], 'expected template names'); - - assert.deepEqual(modelNames, [ - 'PostsArray', - ], 'expected model names'); - - assert.deepEqual(viewClassNames, [ - 'App.PostsView', - ], 'expected view class names'); - - assert.deepEqual(durations, [ - '1.00ms', - ], 'expected render durations'); - - let titleTips = [...findAll('span[title]')].map(node => node.getAttribute('title')).sort(); - - assert.deepEqual(titleTips, [ - 'App.PostsController', - 'App.PostsView', - 'PostsArray', - 'posts', - 'posts' - ], 'expected title tips'); - }); - - test("It should clear the search filter when the clear button is clicked", async function(assert) { - let viewTree = defaultViewTree(); - - await visit('/view-tree'); - run(() => { - port.trigger('view:viewTree', { tree: viewTree }); - }); - await wait(); - - let treeNodes = findAll('.js-view-tree-item'); - assert.equal(treeNodes.length, 3, 'expected all tree nodes'); - - await fillIn('.js-filter-views input', 'post'); - treeNodes = findAll('.js-view-tree-item'); - assert.equal(treeNodes.length, 1, 'expected filtered tree nodes'); - - await click('.js-search-field-clear-button'); - treeNodes = findAll('.js-view-tree-item'); - assert.equal(treeNodes.length, 3, 'expected all tree nodes'); - }); - - test("It should update the view tree when the port triggers a change", async function(assert) { - assert.expect(4); - let treeNodes, viewTree = defaultViewTree(); - - await visit('/view-tree'); - run(() => port.trigger('view:viewTree', { tree: viewTree })); - await wait(); - - treeNodes = findAll('.js-view-tree-item'); - assert.equal(treeNodes.length, 3); - let viewControllersEls = findAll('.js-view-controller'); - assert.dom(viewControllersEls[viewControllersEls.length - 1]).hasText('App.CommentsController'); - - viewTree = defaultViewTree(); - viewTree.children.splice(0, 1); - viewTree.children[0].value.controller.name = 'App.SomeController'; - run(() => port.trigger('view:viewTree', { tree: viewTree })); - await wait(); - treeNodes = findAll('.js-view-tree-item'); - assert.equal(treeNodes.length, 2); - viewControllersEls = findAll('.js-view-controller'); - assert.dom(viewControllersEls[viewControllersEls.length - 1]).hasText('App.SomeController'); - }); - - test("Previewing / showing a view on the client", async function(assert) { - let messageSent = null; - port.reopen({ - send(name, message) { - messageSent = { name, message }; - } - }); - - await visit('/view-tree'); - let viewTree = defaultViewTree(); - viewTree.children = []; - run(() => port.trigger('view:viewTree', { tree: viewTree })); - await wait(); - await triggerEvent('.js-view-tree-item', 'mouseenter'); - assert.equal(messageSent.name, 'view:previewLayer', "Client asked to preview layer"); - assert.equal(messageSent.message.objectId, 'applicationView', "Client sent correct id to preview layer"); - await triggerEvent('.js-view-tree-item', 'mouseleave'); - assert.equal(messageSent.name, 'view:hidePreview', "Client asked to hide preview"); - }); - - test("Inspecting views on hover", async function(assert) { - let messageSent = null; - port.reopen({ - send(name, message) { - messageSent = { name, message }; - } - }); - - await visit('/view-tree'); - await click('.js-inspect-views'); - assert.equal(messageSent.name, 'view:inspectViews'); - assert.deepEqual(messageSent.message, { inspect: true }); - run(() => port.trigger('view:startInspecting')); - await wait(); - await click('.js-inspect-views'); - assert.equal(messageSent.name, 'view:inspectViews'); - assert.deepEqual(messageSent.message, { inspect: false }); - }); - - test("Configuring which views to show", async function(assert) { - let messageSent = null; - port.reopen({ - send(name, message) { - messageSent = { name, message }; - } - }); - - await visit('/view-tree'); - await click('.js-filter-components input'); - assert.equal(messageSent.name, 'view:setOptions'); - assert.deepEqual(messageSent.message.options, { components: true }); - assert.equal(messageSent.name, 'view:setOptions'); - assert.deepEqual(messageSent.message.options, { components: true }); - }); - - test("Inspecting a model", async function(assert) { - let messageSent = null; - port.reopen({ - send(name, message) { - messageSent = { name, message }; - } - }); - - await visit('/view-tree'); - let tree = defaultViewTree(); - run(() => { - port.trigger('view:viewTree', { tree }); - }); - await wait(); - let model = find('.js-view-model-clickable'); - await click(model); - assert.equal(messageSent.name, 'objectInspector:inspectById'); - assert.equal(messageSent.message.objectId, 'postsArray'); - }); -});