Skip to content

Commit

Permalink
fix(view-model): add logic to get viewModel from components that are …
Browse files Browse the repository at this point in the history
…NOT @containerless and add tests (#14)

Fixes #13
  • Loading branch information
silbinarywolf authored Feb 13, 2019
1 parent 7a8b69e commit 0245343
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 43 deletions.
64 changes: 64 additions & 0 deletions cypress/integration/unit/view-model.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { PLATFORM } from 'aurelia-framework';
import { bootstrap } from 'aurelia-bootstrapper';
import { StageComponent } from 'cypress-aurelia-unit-test';

import { BoxComponent } from '~/box-component/box-component';
import * as styles from '~/box-component/box-component.scss';
import { ContainerlessDiv } from '~/containerless-div/containerless-div';

describe('ViewModel', () => {
it('Test getting data from viewModel', () => {
const component = StageComponent
.withResources<BoxComponent>(PLATFORM.moduleName('box-component/box-component'))
.inView(`
<box-component
value.bind="value"
></box-component>`)
.boundTo({
value: 'viewModel test'
});
component.create(bootstrap);
cy.get(`.${styles.main}`).then(() => {
if (!component.viewModel) {
expect(component.viewModel).to.not.equal(undefined);
return;
}
expect(component.viewModel.value).to.equal('viewModel test');
});
});

it('Test setting data on viewModel and it updating the view', () => {
const component = StageComponent
.withResources<BoxComponent>(PLATFORM.moduleName('box-component/box-component'))
.inView(`
<box-component
value.bind="value"
></box-component>`)
.boundTo({
value: 'viewModel not setup'
});
component.create(bootstrap);
cy.get(`.${styles.main}`).then(() => {
if (!component.viewModel) {
expect(component.viewModel).to.not.equal(undefined);
return;
}
component.viewModel.value = 'viewModel has been changed!';
});
cy.get(`.${styles.main}`).contains('viewModel has been changed!');
});

it('Check that viewModel is undefined for containerless component', () => {
const component = StageComponent
.withResources<ContainerlessDiv>(PLATFORM.moduleName('containerless-div/containerless-div'))
.inView(`<containerless-div></containerless-div>`);
component.create(bootstrap);
cy.get(`div`).then(() => {
// NOTE: Jake: 2019-02-13
// At the time of writing we retrieve the viewModel from
// the mounted elements after running "enhance()", this means we
// can't get the viewModel for containerless components.
expect(component.viewModel).to.equal(undefined);
});
});
});
98 changes: 85 additions & 13 deletions lib/component-tester.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ import 'reflect-metadata';
import { Aurelia, FrameworkConfiguration } from 'aurelia-framework';
import { DOM } from 'aurelia-pal';

interface AureliaController {
view: {
resources: {
viewUrl: string
}
};
viewModel: any;
}

interface Node {
firstChild: Node | null;
nextSibling: Node | null;
au?: { [name: string]: AureliaController | undefined };
}

// NOTE(Jake): 2019-01-07
// Used by aurelia-testing. Keeping incase we want to reintroduce.
// interface AureliaWithRoot extends Aurelia {
Expand Down Expand Up @@ -58,6 +73,20 @@ function copyStyles(componentName: string): void {
});
}

// walkNode recursively
function walkNode(node: Node | null, func: (node: Node) => void): void {
if (!node ||
!node.firstChild) {
return;
}
func(node);
node = node.firstChild;
while (node) {
walkNode(node, func);
node = node.nextSibling;
}
}

function newPatches() {
return {
aureliaDialogDisabled: false
Expand All @@ -77,16 +106,19 @@ export class StageComponent {
// NOTE: Jake: 2018-12-19
// Clear document.body, this is so the first step of 2nd test won't have
// the previous test rendered in it when you're debugging.
const document: Document = (cy as any).state('document');
while (document.body.firstChild) {
document.body.removeChild(document.body.firstChild);
const doc: Document = (cy as any).state('document');
while (doc.body.firstChild) {
doc.body.removeChild(doc.body.firstChild);
}

return new ComponentTester().withResources(resources);
}
}

export class ComponentTester<T = any> {
/**
* The class of the component. This is value is undefined for containerless components.
*/
public viewModel?: T;

// public element: Element;
Expand Down Expand Up @@ -134,21 +166,24 @@ export class ComponentTester<T = any> {
// TS4053: Return type of public method from exported class has or is using name 'Bluebird' from external module
// "cypress-aurelia-unit-test/node_modules/cypress/node_modules/@types/bluebird/index" but cannot be named.
return new Cypress.Promise((resolve, reject) => {
const document: Document = (cy as any).state('document');
const doc: Document = (cy as any).state('document');
return bootstrap((aurelia: Aurelia) => {
return Promise.resolve(this.configure(aurelia)).then(() => {
if (this.resources) {
aurelia.use.globalResources(this.resources);
}

// Reset viewModel
this.viewModel = undefined;

// NOTE(Jake): 2018-12-18
// Fixes "inner error: TypeError: Illegal constructor"
// Modified answer from: https://github.com/aurelia/framework/issues/382
aurelia.container.registerInstance(Element, document.createElement);
aurelia.container.registerInstance(Element, doc.createElement);

// NOTE(Jake): 2018-12-20
// Fix cases where a user has used @inject(DOM.Element)
aurelia.container.registerInstance(DOM.Element, document.createElement);
aurelia.container.registerInstance(DOM.Element, doc.createElement);

// Remove any plugins that don't work or cause crashes
{
Expand Down Expand Up @@ -182,30 +217,67 @@ export class ComponentTester<T = any> {
}

return aurelia.start().then(() => {
if (document.activeElement !== document.body) {
if (doc.activeElement !== doc.body) {
// Reset focus to body element if it's not on it
document.body.focus();
doc.body.focus();
}

// Clear the document body and add child
while (document.body.firstChild) {
document.body.removeChild(document.body.firstChild);
while (doc.body.firstChild) {
doc.body.removeChild(doc.body.firstChild);
}
document.body.innerHTML = this.html;
doc.body.innerHTML = this.html;

// NOTE: Jake: 2019-01-31 - #8
// Consider allowing bindingContext to be a function as well.
// A potential benefit is that you can use "Container.instance.get" within the context
const bindingContext = this.bindingContext;
// const bindingContext = typeof(this.bindingContext) === 'function' ? this.bindingContext() : this.bindingContext;

return aurelia.enhance(bindingContext, document.body).then(() => {
const rootElement = doc.body;
return aurelia.enhance(bindingContext, rootElement).then(() => {
// NOTE: Jake: 2018-12-19
// These are in the original aurelia-testing library
// this.rootView = aurelia.root;
// this.element = this.host.firstElementChild as Element;
copyStyles(this.resources.join(','));

// NOTE: Jake: 2019-02-13
// We walk the newly enhanced DOM to find the controller applied
// to the element so that we can extract the viewModel.
walkNode(rootElement, (el) => {
if (!this.viewModel &&
el.au) {
const controllers = el.au;
let controller;
for (const key in controllers) {
if (!controllers.hasOwnProperty(key)) {
continue;
}
controller = controllers[key];
break;
}
if (controller &&
controller.view) {
// NOTE: Jake: 2019-02-13
// There is no way to guarantee the first element we find is the element
// we intended to mount (due to @containerless), so we check that this
// element's template resource reference matches the one we gave.
// We also compare two versions, one string without *.html and one with it
// as both are valid.
const viewUrl = controller.view.resources.viewUrl;
const viewUrlNoExt = viewUrl.split('.').slice(0, -1).join('.');
if (this.resources.indexOf(viewUrl) > -1 ||
this.resources.indexOf(viewUrlNoExt) > -1) {
this.viewModel = controller.viewModel;
}
}
}
});
if (!this.viewModel) {
// tslint:disable-next-line:no-console
console.warn('Unable to determine viewModel for mounted component. This is expected behaviour for @containerless components.');
}

// NOTE: Jake: 2019-01-31 - #9
// We used to call these manually like the aurelia-testing library,
// however aurelia.enhance() calls lifecycle methods already, so this was
Expand Down
Loading

0 comments on commit 0245343

Please sign in to comment.