Skip to content

Commit

Permalink
fix(iframe): add redirections for appendChild/querySelector/querySele…
Browse files Browse the repository at this point in the history
…ctorAll to use main application iframe (#15)

* feat(iframe): add redirections for appendChild/querySelector/querySelectorAll to use main application iframe

* fix(iframe): fix logic in copyStyles to use "document.head" so it doesn't use the overwritten methods
  • Loading branch information
silbinarywolf authored Feb 20, 2019
1 parent 0245343 commit 4d1e7d1
Show file tree
Hide file tree
Showing 8 changed files with 237 additions and 42 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@
This is a library designed to feel like the official [aurelia/testing](https://github.com/aurelia/testing) library but instead it is built to work with Cypress.
The purpose of this module is so that components can be visually unit tested as close to reality as possible, ie. in a browser rather than in NodeJS with a virtual DOM.

## Known Limitations

- `viewModel` is `undefined` in Cypress if a component has been annotated with @containerless.
- Appending elements directly to the `document.body` may not work as expected due to the fact that Cypress sandboxes it's test code and application code into seperate iframes as it was not currently designed for you to import components directly. To work around this limitation there is currently some code in place which overrides the functionality of the following methods to redirect calls to the main application iframe.
- `document.addEventListener`
- This was added to fix `click.delegate` so that click events were processed as expected.
- `document.appendChild`
- `document.querySelector`
- `document.querySelectorAll`
- `document.body.appendChild`
- `document.body.querySelector`
- `document.body.querySelectorAll`

## Credits

- [Gleb Bahmutov](https://github.com/bahmutov/cypress-react-unit-test) for their Cypress React Unit Test library. It helped me figure out how to get styles working quickly.
43 changes: 43 additions & 0 deletions cypress/integration/unit/iframe-fixes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { PLATFORM } from 'aurelia-framework';
import { bootstrap } from 'aurelia-bootstrapper';
import { ComponentTester, StageComponent } from 'cypress-aurelia-unit-test';

import { configurationWithoutStart } from '~/main';
import { DocumentManipulator } from '~/document-manipulator/document-manipulator';

describe('IframeFixes', () => {
let component: ComponentTester<DocumentManipulator>;

beforeEach(() => {
component = StageComponent
.withResources(PLATFORM.moduleName('document-manipulator/document-manipulator'))
.inView(`
<document-manipulator></document-manipulator>`)
.bootstrap(configurationWithoutStart);
});

it('Check document.body.appendChild override is active', () => {
component.create(bootstrap);
cy.get('.bodyAppendChild').should('exist');
});

it('Check document.querySelector override is active', () => {
component.create(bootstrap);
cy.get('.documentQuerySelectorSuccess').should('exist');
});

it('Check document.body.querySelector override is active', () => {
component.create(bootstrap);
cy.get('.documentBodyQuerySelectorSuccess').should('exist');
});

it('Check document.querySelectorAll override is active', () => {
component.create(bootstrap);
cy.get('.documentQueryAllSelectorSuccess').should('exist');
});

it('Check document.body.querySelectorAll override is active', () => {
component.create(bootstrap);
cy.get('.documentBodyQueryAllSelectorSuccess').should('exist');
});
});
4 changes: 2 additions & 2 deletions lib/component-tester.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const aureliaDialogWarningMessage = 'aurelia-dialog causes Cypress to crash. Rem

// having weak reference to styles prevents garbage collection
// and "losing" styles when the next test starts
const stylesCache: Map<string, NodeListOf<Element>> = new Map<string, NodeListOf<Element>>();
const stylesCache: Map<string, NodeListOf<HTMLStyleElement>> = new Map<string, NodeListOf<HTMLStyleElement>>();

// NOTE: Jake: 2018-12-18
// Taken from: https://github.com/bahmutov/cypress-react-unit-test/blob/master/lib/index.js
Expand All @@ -45,7 +45,7 @@ function copyStyles(componentName: string): void {
// like component name
const hash = componentName;

let styles = document.querySelectorAll('head style');
let styles = document.head.querySelectorAll('style');
if (styles.length) {
// tslint:disable-next-line:no-console
console.log('Injected %d styles into view', styles.length);
Expand Down
39 changes: 0 additions & 39 deletions lib/event-listener-fixes.ts

This file was deleted.

102 changes: 102 additions & 0 deletions lib/iframe-fixes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
function overrideSpecIFrameToApplyToAppIFrame(): void {
const specIframe = window.parent.document.querySelector('iframe.spec-iframe');
if (!specIframe) {
throw new Error('Cannot find .spec-iframe');
}
const specDocument: Document = (specIframe as any).contentDocument;
if (!specDocument) {
throw new Error('Cannot find contentDocument on specIframe');
}

const rootDocument = window.parent.document;
const appIframe = rootDocument.querySelector('iframe.aut-iframe');
if (!appIframe) {
throw new Error('Cannot find .aut-iframe');
}
const appDocument: Document = (appIframe as any).contentDocument;
if (!appDocument) {
throw new Error('Cannot find contentDocument on appIframe');
}

// Override addEventListener on spec iframe so that we can attach those
// same events to the app iframe, this fixes:
// - aurelia-binding - click.delegate won't work in templates without this.
{
const originalEventListener = specDocument.addEventListener;
const addEventListener = function(this: any): void {
originalEventListener.apply(this, arguments as any);
appDocument.addEventListener.apply(appDocument, arguments as any);
};
if (specDocument.addEventListener !== addEventListener) {
specDocument.addEventListener = addEventListener;
}
}

// Override document.appendChild on spec iframe so that we can append those
// elements to the app iframe.
{
const documentAppendChild = function <T extends Node>(this: T): T {
return appDocument.appendChild.apply(appDocument, arguments as any) as T;
};
if (specDocument.appendChild !== documentAppendChild) {
specDocument.appendChild = documentAppendChild;
}
}

// Override document.body.appendChild on spec iframe so that we can append those
// elements to the app iframe.
{
const bodyAppendChild = function <T extends Node>(this: T): T {
return appDocument.body.appendChild.apply(appDocument.body, arguments as any) as T;
};
if (specDocument.body.appendChild !== bodyAppendChild) {
specDocument.body.appendChild = bodyAppendChild;
}
}

// Override document.querySelector on spec iframe so that we can query for
// elements that have been rendered in the app iframe.
{
const documentQuerySelector = function(this: any): Element | null {
return appDocument.querySelector.apply(appDocument, arguments as any);
};
if (specDocument.querySelector !== documentQuerySelector) {
specDocument.querySelector = documentQuerySelector;
}
}

// Override document.body.querySelector on spec iframe so that we can query for
// elements that have been rendered in the app iframe.
{
const bodyQuerySelector = function(this: any): Element | null {
return appDocument.body.querySelector.apply(appDocument.body, arguments as any);
};
if (specDocument.body.querySelector !== bodyQuerySelector) {
specDocument.body.querySelector = bodyQuerySelector;
}
}

// Override document.querySelectorAll on spec iframe so that we can query for
// elements that have been rendered in the app iframe.
{
const documentQueryAllSelector = function(this: any): NodeListOf<Element> {
return appDocument.querySelectorAll.apply(appDocument, arguments as any);
};
if (specDocument.querySelectorAll !== documentQueryAllSelector) {
specDocument.querySelectorAll = documentQueryAllSelector;
}
}

// Override document.querySelectorAll on spec iframe so that we can query for
// elements that have been rendered in the app iframe.
{
const bodyQueryAllSelector = function(this: any): NodeListOf<Element> {
return appDocument.body.querySelectorAll.apply(appDocument.body, arguments as any);
};
if (specDocument.body.querySelectorAll !== bodyQueryAllSelector) {
specDocument.body.querySelectorAll = bodyQueryAllSelector;
}
}
}

before(overrideSpecIFrameToApplyToAppIFrame);
2 changes: 1 addition & 1 deletion lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
import './event-listener-fixes';
import './iframe-fixes';
export * from './component-tester';
29 changes: 29 additions & 0 deletions src/document-manipulator/document-manipulator.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<template>
<div>Document Manipulator</div>
<div>This component exists to test if various "document" functions have been overridden to point from the Spec IFrame to the App IFrame.</div>
<hr/>
<div
if.bind="documentQuerySelectorSuccess"
class="documentQuerySelectorSuccess"
>
- documentQuerySelector
</div>
<div
if.bind="documentBodyQuerySelectorSuccess"
class="documentBodyQuerySelectorSuccess"
>
- documentBodyQuerySelector
</div>
<div
if.bind="documentQueryAllSelectorSuccess"
class="documentQueryAllSelectorSuccess"
>
- documentQueryAllSelector
</div>
<div
if.bind="documentBodyQueryAllSelectorSuccess"
class="documentBodyQueryAllSelectorSuccess"
>
- documentBodyQueryAllSelector
</div>
</template>
47 changes: 47 additions & 0 deletions src/document-manipulator/document-manipulator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { autoinject } from 'aurelia-framework';

@autoinject
export class DocumentManipulator {
protected documentQuerySelectorSuccess: boolean = false;

protected documentBodyQuerySelectorSuccess: boolean = false;

protected documentQueryAllSelectorSuccess: boolean = false;

protected documentBodyQueryAllSelectorSuccess: boolean = false;

protected attached(): void {
// NOTE(Jake): 2019-02-20
// We create a bunch of elements that manipulate 'document'
// directly. This is to test that our code in 'iframe-fixes.ts' works.

{
const el = document.createElement('div');
el.classList.add('bodyAppendChild');
el.textContent = '- bodyAppendChild';
document.body.appendChild(el);
}

{
const el = document.querySelector('document-manipulator');
if (el) {
this.documentQuerySelectorSuccess = true;
}
}

{
const el = document.body.querySelector('document-manipulator');
if (el) {
this.documentBodyQuerySelectorSuccess = true;
}
}

if (document.querySelectorAll('document-manipulator').length > 0) {
this.documentQueryAllSelectorSuccess = true;
}

if (document.body.querySelectorAll('document-manipulator').length > 0) {
this.documentBodyQueryAllSelectorSuccess = true;
}
}
}

0 comments on commit 4d1e7d1

Please sign in to comment.