I am trying to run Cypress component testing on a library of Angular components. Currently, all of my components have shadowDom turned on. Which seems to be causing me a problem when running multiple tests on a single component, as cypress appears to be struggling to destroy shadowDom components between it
statements.
I tried three different scenarios to get something that works and they all have problems, which I will outline below. The questions I have are:
I have made a minimum reproduction here: https://github.com/jclark86613/cypress-shadow-comps
I mount the component in a beforeEach
the first test will pass, but the second (identical) test will fail.
ShadowDom = false, this scenario works ShadowDom = true, this scenario does not work
describe('TestShadowComponent', () => {
beforeEach(() => {
mount(TestShadowComponent);
})
it('should display the title', () => {
cy.get('button', {includeShadowDom: true}).should('have.text', 'My Button')
})
it('should display the title', () => {
cy.get('button', {includeShadowDom: true}).should('have.text', 'My Button')
})
})
TypeError
Cannot set property message of [object DOMException] which has only a getter
Because this error occurred during a before each hook we are skipping the remaining tests in the current suite: TestShadowComponent
I mount the component in each it
block. This also always fails on the second identical test, but with a different error.
ShadowDom = false, this senario works ShadowDom = true, this senario does not work
describe('TestShadowComponent', () => {
it('should display the title', () => {
mount(TestShadowComponent);
cy.get('button', {includeShadowDom: true}).should('have.text', 'My Button')
})
it('should display the title', () => {
mount(TestShadowComponent);
cy.get('button', {includeShadowDom: true}).should('have.text', 'My Button')
})
})
NotSupportedError
Failed to execute 'attachShadow' on 'Element': Shadow root cannot be created on a host which already hosts a shadow tree.
I mount the component once in a before
block. This scenario works for my basic tests, but they no longer detect the output spies. I suspect that using before
to mount a component is bad practice as it can allow state to bleed between tests. however, if turn shadowDom off and change before
to beforeEach
this scenario begins to work.
describe('TestShadowComponent', () => {
before(() => {
mount(TestShadowComponent, {autoSpyOutputs: true});
})
it('should display the title', () => {
cy.get('button', {includeShadowDom: true}).should('have.text', 'My Button')
})
it('should display the title', () => {
cy.get('button', {includeShadowDom: true}).should('have.text', 'My Button')
})
it('should click the button', () => {
cy.get('button', {includeShadowDom: true}).click()
// does click spy exists
cy.get('@clickSpy').should('have.been.called')
})
})
CypressError
cy.get() could not find a registered alias for: @clickSpy.
You have not aliased anything yet.
The problem is with the un-mounting of the component between tests, which Cypress does to have a clean slate for each new test.
It looks like it removes the TestShadowComponent
but not the shadow-root
attached to the parent element (the mounting point withing the web page).
Hence the message Shadow root cannot be created on a host which already hosts a shadow tree when the second mount(TestShadowComponent)
is called.
Cypress does not expect the component to make changes to the mounting point, but the Angular option encapsulation: ViewEncapsulation.ShadowDom
adds the shadow-root
in order to protect styles from bleeding over to other components
This is the HTML you see at runtime during the test:
<div data-cy-root id="root0" ng-version="17.3.11"> // supplied by Cypress
#shadow-root (open) // Angular attaches shadow-root
<style></style> // and adds styles inside
<button>My Button</button> // this is the component
</div>
You can fix it by doing additional cleanup in an afterEach()
hook
describe('TestShadowComponent', () => {
afterEach(() => {
cy.get('[data-cy-root]') // get the attachment point element
.then($el => {
const el = $el[0] // working with raw element
const newRoot = el.cloneNode() // make a clone
el.parentElement!.appendChild(newRoot) // add it to the page
el.remove() // remove the original
})
})
it('should display the title', () => {
mount(TestShadowComponent)
cy.get('button', {includeShadowDom: true}).should('have.text', 'My Button')
})
it('should display the title', () => {
mount(TestShadowComponent)
cy.get('button', {includeShadowDom: true}).should('have.text', 'My Button')
})
})
Refs
The Cypress cleanup code: npm/angular/src/mount.ts
function cleanup () {
// Not public, we need to call this to remove the last component from the DOM
try {
(getTestBed() as any).tearDownTestingModule()
} catch (e) {
const notSupportedError = new Error(`Failed to teardown component. The version of Angular you are using may not be officially supported.`)
;(notSupportedError as any).docsUrl = 'https://on.cypress.io/component-framework-configuration'
throw notSupportedError
}
getTestBed().resetTestingModule()
activeFixture = null
}
You can also use a wrapper component inside the mount()
as a sort of buffer between the component you want to test and the Cypress attachment element.
That way, the wrapper takes the shadow-root
instead of the data-cy-root
element, and it is removed along with TestShadowComponent
during the cleanup.
The runtime HTML now looks like this (compare it to above)
<div data-cy-root id="root0" ng-version="17.3.11">
<app-test-shadow>
#shadow-root (open)
<style></style>
<button>My Button</button>
</app-test-shadow>
</div>
Implementation:
@Component({
template: `<app-test-shadow />`
})
class WrapperComponent {}
describe('with wrapper', () => {
it('should display the title', () => {
cy.mount(WrapperComponent, {
declarations: [TestShadowComponent],
})
cy.get('button', {includeShadowDom: true}).should('have.text', 'My Button')
})
it('should display the title', () => {
cy.mount(WrapperComponent, {
declarations: [TestShadowComponent],
})
cy.get('button', {includeShadowDom: true}).should('have.text', 'My Button')
})
})
Ref Cypress example:
cypress-component-testing-apps/angular/src/app/button
/button.component.cy.ts