使用故事书和柏树测试角度组件@Output



我正在尝试测试角度组件的输出。

我有一个复选框组件,它使用事件发射器输出其值。复选框组件包装在故事书故事中,用于演示和测试目的:

export const basic = () => ({
moduleMetadata: {
imports: [InputCheckboxModule],
},
template: `
<div style="color: orange">
<checkbox (changeValue)="changeValue($event)" [selected]="checked" label="Awesome">
</checkbox>
</div>`,
props: {
checked: boolean('checked', true),
changeValue: action('Value Changed'),
},
});

我正在使用一个动作来捕获值更改并将其记录到屏幕上。

但是,在为此组件编写赛普拉斯 e2e 时,我只使用 iFrame 而不是整个故事书应用程序。

我想找到一种方法来测试输出是否正常工作。我尝试在iFrame中的postMessage方法上使用间谍,但这不起作用。

beforeEach(() => {
cy.visit('/iframe.html?id=inputcheckboxcomponent--basic', {
onBeforeLoad(win) {
cy.spy(window, 'postMessage').as('postMessage');
},
});
});

然后断言将是:

cy.get('@postMessage').should('be.called');

有没有其他方法可以断言(changeValue)="changeValue($event)"开火了?

更新 07.05.2022:通过故事书的@storybook/addon-actions

灵感来自@jb17的答案和柏树故事书。

/**
* my-component.component.ts
*/
@Component({
selector: 'my-component',
template: `<button (click)="outputChange.emit('test-argument')"></button>`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MyComponent {
@Output()
outputChange = new EventEmitter<string>();
}
/**
* my-component.stories.ts
*/
export default {
title: 'MyComponent',
component: MyComponent,
argTypes: {
outputChange: { action: 'outputChange' },
},
} as Meta<MyComponent>;
const Template: Story<MyComponent> = (args: MyComponent) => ({
props: args,
});
export const Primary = Template.bind({});
Primary.args = {};
/**
* my-component.spec.ts
*/
describe('MyComponent @Output Test', () => {
beforeEach(() =>
cy.visit('/iframe.html?id=mycomponent--primary', {
onLoad: registerActionsAsAlias(), // ❗️  
})
);
it('triggers output', () => {
cy.get('button').click();
// Get spy via alias set by `registerActionsAsAlias()`
cy.get('@outputChange').should('have.been.calledWith', 'test-argument');
});
});
/**
* somewhere.ts
*/
import { ActionDisplay } from '@storybook/addon-actions';
import { AddonStore } from '@storybook/addons';
export function registerActionsAsAlias(): (win: Cypress.AUTWindow) => void {
// Store spies in the returned functions' closure
const actionSpies = {};
return (win: Cypress.AUTWindow) => {
// https://github.com/storybookjs/storybook/blob/master/lib/addons/src/index.ts
const addons: AddonStore = win['__STORYBOOK_ADDONS'];
if (addons) {
// https://github.com/storybookjs/storybook/blob/master/addons/actions/src/constants.ts
addons.getChannel().addListener('storybook/actions/action-event', (event: ActionDisplay) => {
if (!actionSpies[event.data.name]) {
actionSpies[event.data.name] = cy.spy().as(event.data.name);
}
actionSpies[event.data.name](event.data.args);
});
}
};
}

方法1:模板

我们可以将最后一个发出的值绑定到模板并检查它。

{
moduleMetadata: { imports: [InputCheckboxModule] },
template: `
<checkbox (changeValue)="value = $event" [selected]="checked" label="Awesome">
</checkbox>

<div id="changeValue">{{ value }}</div> <!-- ❗️   -->
`,
}
it("emits `changeValue`", () => {
// ...
cy.get("#changeValue").contains("true"); // ❗️  
});

方法2:窗口

我们可以将最后一个发出的值分配给全局window对象,在 Cypress 中检索它并验证该值。

export default {
title: "InputCheckbox",
component: InputCheckboxComponent,
argTypes: {
selected: { type: "boolean", defaultValue: false },
label: { type: "string", defaultValue: "Default label" },
},
} as Meta;

const Template: Story<InputCheckboxComponent> = (
args: InputCheckboxComponent
) =>
({
moduleMetadata: { imports: [InputCheckboxModule] },
component: InputCheckboxComponent,
props: args,
} as StoryFnAngularReturnType);

export const E2E = Template.bind({});
E2E.args = {
label: 'E2e label',
selected: true,
changeValue: value => (window.changeValue = value), // ❗️  
};
it("emits `changeValue`", () => {
// ...
cy.window().its("changeValue").should("equal", true); // ❗️  
});

方法3:角度

我们可以使用存储在全局命名空间中的 Angular 函数ng来获取对 Angular 组件的引用并监视输出。

⚠️ 注意力:

  • ng.getComponent()仅在 Angular 以开发模式运行时可用。 即 不调用enableProdMode()
  • .storybook/main.js中设置process.env.NODE_ENV = "development";以防止故事书在生产模式下构建 Angular(请参阅源代码)。
export const E2E = Template.bind({});
E2E.args = {
label: 'E2e label',
selected: true,
// Story stays unchanged
};
describe("InputCheckbox", () => {
beforeEach(() => {
cy.visit(
"/iframe.html?id=inputcheckboxcomponent--e-2-e",
registerComponentOutputs("checkbox") // ❗️  
);
});
it("emits `changeValue`", () => {
// ...
cy.get("@changeValue").should("be.calledWith", true); // ❗️  
});
});
function registerComponentOutputs(
componentSelector: string
): Partial<Cypress.VisitOptions> {
return {
// https://docs.cypress.io/api/commands/visit.html#Provide-an-onLoad-callback-function
onLoad(win) {
const componentElement: HTMLElement = win.document.querySelector(
componentSelector
);
// https://angular.io/api/core/global/ngGetComponent
const component = win.ng.getComponent(componentElement);
// Spy on all `EventEmitters` (i.e. `emit()`) and create equally named alias
Object.keys(component)
.filter(key => !!component[key].emit)
.forEach(key => cy.spy(component[key], "emit").as(key)); // ❗️  
},
};
}

总结

  • 我喜欢方法1中没有魔法。它易于阅读和理解。遗憾的是,它需要指定一个模板,其中包含用于验证输出的附加元素。
  • 方法 2 的优点是我们不再需要指定模板。但是我们需要为每个@Output添加想要测试其他代码的内容。此外,它使用全局window来"通信"。
  • Apprach 3 也不需要模板。它的优点是故事书代码(故事)不需要任何调整。我们只需要将一个参数传递给cy.visit()(很可能已经使用)即可执行检查。因此,如果我们想通过 Storybook 的iframe测试更多组件,感觉就像一个可扩展的解决方案。最后但并非最不重要的一点是,我们检索对 Angular 组件的引用。这样,我们还可以直接在组件本身上调用方法或设置属性。这与ng.applyChanges相结合,似乎为其他测试用例打开了一些大门。

您正在监视window.postMessage(),这是一种在窗口对象(弹出窗口,页面,iframe等)之间实现跨源通信的方法。

故事书中的 iFrame 不会将任何消息传达给另一个窗口对象,但您可以在应用程序上安装 Kuker 或其他外部 Web 调试器来监视两者之间的消息,从而使 Cypress 间谍方法正常工作。

如果您选择在 angular 应用程序上安装 Kuker,以下是操作方法:

npm install -S kuker-emitters

添加Kuker Chrome扩展程序以使其工作。

如果您使用的是 cypress-storybook 包和 @storybook/addon-actions,则有一种方法可用于此用例,在我看来,它提供了最简单的解决方案。

使用故事书操作插件,您可以像这样声明您的@Output事件

export default {
title: 'Components/YourComponent',
component: YourComponent,
decorators: [
moduleMetadata({
imports: [YourModule]
})
]
} as Meta;
const Template: Story<YourStory> = (args: YourComponent) => ({
props: args
});
export const default = Template.bind({});
default.args = {
// ...
changeValue: action('Value Changed'), // action from @storybook/addon-actions
};

在 cypress 测试中,您现在可以调用cy.storyAction()方法并对其应用 expect 语句。

it('should execute event', () => {
// ...
cy.storyAction('Value Changed').should('have.been.calledWith', 'value');
})

相关内容

最新更新