使用 Storybook 和 Cypress 测试角度分量 @Output

Wim*_*oet 5 angular storybook cypress storybook-addon-specifications angular-storybook

我正在尝试测试角度分量的输出。

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

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'),
  },
});
Run Code Online (Sandbox Code Playgroud)

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

然而,当为此组件编写 cypress e2e 时,我仅使用 iFrame 而不是整个故事书应用程序。

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

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

然后断言将是:

  cy.get('@postMessage').should('be.called');
Run Code Online (Sandbox Code Playgroud)

还有其他方法可以断言已(changeValue)="changeValue($event)" 触发吗?

Phi*_*ppe 5

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

\n

受到@jb17的回答和cypress-storybook的启发。

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

方法一:模板

\n

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

\n
{\n  moduleMetadata: { imports: [InputCheckboxModule] },\n  template: `\n   <checkbox (changeValue)="value = $event" [selected]="checked" label="Awesome">\n   </checkbox>\n  \n   <div id="changeValue">{{ value }}</div> <!-- \xe2\x9d\x97\xef\xb8\x8f -->\n `,\n}\n
Run Code Online (Sandbox Code Playgroud)\n
it("emits `changeValue`", () => {\n // ...\n\n cy.get("#changeValue").contains("true"); // \xe2\x9d\x97\xef\xb8\x8f\n});\n\n
Run Code Online (Sandbox Code Playgroud)\n

方法二:窗口

\n

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

\n
export default {\n  title: "InputCheckbox",\n  component: InputCheckboxComponent,\n  argTypes: {\n    selected: { type: "boolean", defaultValue: false },\n    label: { type: "string", defaultValue: "Default label" },\n  },\n} as Meta;\n\n\nconst Template: Story<InputCheckboxComponent> = (\n  args: InputCheckboxComponent\n) =>\n  ({\n    moduleMetadata: { imports: [InputCheckboxModule] },\n    component: InputCheckboxComponent,\n    props: args,\n  } as StoryFnAngularReturnType);\n\n\nexport const E2E = Template.bind({});\nE2E.args = {\n  label: \'E2e label\',\n  selected: true,\n  changeValue: value => (window.changeValue = value), // \xe2\x9d\x97\xef\xb8\x8f\n};\n\n
Run Code Online (Sandbox Code Playgroud)\n
it("emits `changeValue`", () => {\n  // ...\n\n  cy.window().its("changeValue").should("equal", true); // \xe2\x9d\x97\xef\xb8\x8f\n});\n
Run Code Online (Sandbox Code Playgroud)\n

方法 3:角度

\n

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

\n

\xe2\x9a\xa0\xef\xb8\x8f 注意:

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

概括

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


小智 0

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

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

如果您选择在 Angular 应用程序上安装 Kuker,请按以下步骤操作:

npm install -S kuker-emitters
Run Code Online (Sandbox Code Playgroud)

还要添加 Kuker Chrome 扩展以使其正常工作。