Upgrade to Pro — share decks privately, control downloads, hide ads and more …

LIFF Component Testing with Cypress

LIFF Component Testing with Cypress

LINE Developers Thailand

September 13, 2020
Tweet

More Decks by LINE Developers Thailand

Other Decks in Technology

Transcript

  1. > Mobile & External Browsers Supported > New APIs: shareTargetPicker,

    ready, getLineVersion etc. > npm install @line/liff // since LIFF 2.3 LIFF 2020 Brief Updates
  2. High Fidelity, Low Speed Low Fidelity, Very Fast UNIT TESTS

    INTEGRATION TESTS E2E TESTS Automated Testing Strategy
  3. “It would be great if we have unit tests with

    Higher FIDELITY and Good SPEED”
  4. Free & Open Source (MIT License) A tool to quickly

    and reliably test anything that runs in a browser.
  5. All-in-one testing tool. Provides popular off-the-shelf Mocha, Sinon and Chai

    and more bundled test utilities $ npm install -D cypress
  6. Your App & Your Tests Same Run Loop Provisioning &

    Perform system-level tasks How Cypress Works ?
  7. describe('Demo', () => { it('is demo', () => { cy.get('[data-testid=input]').click().type('Hello

    World'); cy.get('[data-testid=input]').should('have.text', 'Hello World'); }); }); Finding Elements Performing Actions Assertion Cypress API
  8. LIFF SDK 1. Dependency with LINE Platform 2. Emulate Opening

    LIFF from LINE App is so difficult 3. Negative Test ??? LIFF Testing Problems
  9. > Cypress automatically includes Sinon.js for stub & spy methods

    > Expose it as Cypress.Sinon or cy.stub() and cy.spy() > Stub: a way to modify a function and control its behavior by your tests > Spy: Letting you capture and assert the function called with correct arguments Control LIFF SDK With Cypress
  10. sh$ vue add cypress-experimental # vue-cli > 3 > A

    Cypress Test Helper to Unit Test Vue Components > Build on top of vue-test-utils > Runs natively on a real browser > Requires Cypress 4.5.0 or later, Node 8+ https://github.com/bahmutov/cypress-vue-unit-test Introducing Cypress Vue Unit Tests
  11. <template> <h1 data-testid="title">Hello {{ name }}!</h1> </template> <script> export default

    { name: 'Greetings', props: { name: { type: String, required: true } } } </script> Greetings.vue Basic Example
  12. Greetings.spec.js import { mount } from 'cypress-vue-unit-test' import Greetings from

    '../../src/components/Greetings.vue' describe('Greetings', () => { it('Works awesomely', () => { const name = 'Cypress'; mount(Greetings, { propsData: { name, }, }); cy.get('[data-testid=title]').should('exist') .and('have.text', `Hello ${name}!`); }); }); Basic Example (TEST)
  13. App.vue created: async function () { try { await liff.init({

    liffId: process.env.VUE_APP_LINE_LIFF_ID, }); } catch (error) { this.$swal({ icon: 'error', title: '<h2 data-testid="errorTitle">LIFF init failed</h2>', html: `<span data-testid="errorMsg">${error.message}</span>`, }); } } Scenario 1: Stub LIFF initialization LIFF Component Testing Examples
  14. App.spec.js describe('App', () => { it('liff init success', () =>

    { cy.stub(liff, 'init').as('liffInitStub').resolves(); mount(App); cy.get('[data-testid="init"]').should('be.visible') .and('have.text', 'Init success'); cy.get('@liffInitStub').should('be.calledWithExactly', { liffId: process.env.VUE_APP_LINE_LIFF_ID }); }); }); Scenario 1: Stub LIFF initialization LIFF Component Testing Examples
  15. App.spec.js describe('App', () => { it('liff init success', () =>

    { cy.stub(liff, 'init').as('liffInitStub').resolves(); mount(App); cy.get('[data-testid="init"]').should('be.visible') .and('have.text', 'Init success'); cy.get('@liffInitStub').should('be.calledWithExactly', { liffId: process.env.VUE_APP_LINE_LIFF_ID }); }); }); Scenario 1: Stub LIFF initialization LIFF Component Testing Examples
  16. App.spec.js describe('App', () => { it('liff init success', () =>

    { cy.stub(liff, 'init').as('liffInitStub').resolves(); mount(App); cy.get('[data-testid="init"]').should('be.visible') .and('have.text', 'Init success'); cy.get('@liffInitStub').should('be.calledWithExactly', { liffId: process.env.VUE_APP_LINE_LIFF_ID }); }); }); Scenario 1: Stub LIFF initialization LIFF Component Testing Examples
  17. App.spec.js describe('App', () => { it('liff init failure', () =>

    { const errorMessage = 'Init Error!'; cy.stub(liff, 'init').as('liffInitFailure').rejects(new Error(errorMessage)); mount(App); cy.get('[data-testid="errorTitle"').should('be.visible') .and('have.text', 'LIFF init failed'); cy.get('[data-testid="errorMsg"]').should('be.visible') .and('have.text', errorMessage); }); }); Scenario 2: LIFF Init Failure Negative Test with LIFF SDK
  18. Scenario 2: LIFF Init Failure Negative Test with LIFF SDK

    App.spec.js describe('App', () => { it('liff init failure', () => { const errorMessage = 'Init Error!'; cy.stub(liff, 'init').as('liffInitFailure').rejects(new Error(errorMessage)); mount(App); cy.get('[data-testid="errorTitle"').should('be.visible') .and('have.text', 'LIFF init failed'); cy.get('[data-testid="errorMsg"]').should('be.visible') .and('have.text', errorMessage); }); });
  19. async created() { await liff.ready; if (!liff.isLoggedIn()) { liff.login(); }

    else { const profile = await liff.getProfile(); this.displayName = profile.displayName; this.pictureUrl = profile.pictureUrl; } }, Profile.vue Authentication with LINE Login
  20. describe('Profile', () => { beforeEach(() => { Cypress.sinon.replace(liff, 'ready', Promise.resolve());

    }); it('render displayName when logged-in', () => { const profile = { displayName: 'Test User', pictureUrl: '<picturlUrl>' } cy.stub(liff, 'isLoggedIn').as('stubIsLoggedIn').returns(true); cy.stub(liff, 'getProfile').as('stubGetProfile').resolves(profile); mount(Profile).then(() => { // access everything in vue instance expect(Cypress.vue.$data.pictureUrl).to.equal(profile.pictureUrl); expect(Cypress.vue.$data.displayName).to.equal(profile.displayName); }); cy.get('[data-testid="pictureUrl"]').should('have.attr', 'src', profile.pictureUrl); cy.get('[data-testid="name"]').should('have.text', `Name: ${profile.displayName}`); }); }); Profile.spec.js Authentication with LINE Login
  21. describe('Profile', () => { beforeEach(() => { Cypress.sinon.replace(liff, 'ready', Promise.resolve());

    }); it('render displayName when logged-in', () => { const profile = { displayName: 'Test User', pictureUrl: '<picturlUrl>' } cy.stub(liff, 'isLoggedIn').as('stubIsLoggedIn').returns(true); cy.stub(liff, 'getProfile').as('stubGetProfile').resolves(profile); mount(Profile).then(() => { // access everything in vue instance expect(Cypress.vue.$data.pictureUrl).to.equal(profile.pictureUrl); expect(Cypress.vue.$data.displayName).to.equal(profile.displayName); }); cy.get('[data-testid="pictureUrl"]').should('have.attr', 'src', profile.pictureUrl); cy.get('[data-testid="name"]').should('have.text', `Name: ${profile.displayName}`); }); }); Profile.spec.js Authentication with LINE Login
  22. describe('Profile', () => { beforeEach(() => { Cypress.sinon.replace(liff, 'ready', Promise.resolve());

    }); it('render displayName when logged-in', () => { const profile = { displayName: 'Test User', pictureUrl: '<picturlUrl>' } cy.stub(liff, 'isLoggedIn').as('stubIsLoggedIn').returns(true); cy.stub(liff, 'getProfile').as('stubGetProfile').resolves(profile); mount(Profile).then(() => { // access everything in vue instance expect(Cypress.vue.$data.pictureUrl).to.equal(profile.pictureUrl); expect(Cypress.vue.$data.displayName).to.equal(profile.displayName); }); cy.get('[data-testid="pictureUrl"]').should('have.attr', 'src', profile.pictureUrl); cy.get('[data-testid="name"]').should('have.text', `Name: ${profile.displayName}`); }); }); Profile.spec.js Authentication with LINE Login
  23. Emulate Opening LIFF from LINE App export default { name:

    'Mobile', data() { return { isInClient: false, os: '', } }, created() { this.isInClient = liff.isInClient(); this.os = liff.getOS(); } }
  24. Mobile.spec.js describe('Mobile', () => { it('emulates running app in LINE

    App', () => { const ios = 'ios'; cy.stub(liff, 'isInClient').as('isInLineApp').returns(true); cy.stub(liff, 'getOS').as('os').returns(ios); cy.viewport(‘iphone-xr'); mount(Mobile).then(() => { expect(Cypress.vue.$data.isInClient).to.be.true; expect(Cypress.vue.$data.os).to.be.equal(ios); }); }); }); // emulate iPhone-XR resolution // assert Vue data property https://on.cypress.io/viewport Emulate Opening LIFF from LINE App
  25. Mobile.spec.js describe('Mobile', () => { it('emulates running app in LINE

    App', () => { const ios = 'ios'; cy.stub(liff, 'isInClient').as('isInLineApp').returns(true); cy.stub(liff, 'getOS').as('os').returns(ios); cy.viewport(‘iphone-xr'); mount(Mobile).then(() => { expect(Cypress.vue.$data.isInClient).to.be.true; expect(Cypress.vue.$data.os).to.be.equal(ios); }); }); }); // emulate iPhone-XR resolution // assert Vue data property https://on.cypress.io/viewport Emulate Opening LIFF from LINE App
  26. sendMessages: async function () { if (liff.isLoggedIn()) { await liff.sendMessages([

    { type: 'text', text: 'Hello World', } ]); this.$swal({ icon: 'success', title: '<h2 data-testid="sent">Sent!</h2>', }); } } SendMessages.vue Integration with LINE Platform
  27. describe('Send Messages', () => { it('can send message with mock

    function', () => { cy.stub(liff, 'sendMessages').as('sendMessages').resolves(); cy.stub(liff, 'isLoggedIn').as('isLoggedIn').returns(true); mount(SendMessages); cy.get('[data-testid="sendMessages"').click(); cy.get('[data-testid="sent"]').should('have.text', 'Sent!'); cy.get('@sendMessages').should('be.calledWithExactly', [ { type: 'text', text: 'Hello World', } ]); }); }); SendMessages.spec.js Integration with LINE Platform
  28. describe('Send Messages', () => { it('can send message with mock

    function', () => { cy.stub(liff, 'sendMessages').as('sendMessages').resolves(); cy.stub(liff, 'isLoggedIn').as('isLoggedIn').returns(true); mount(SendMessages); cy.get('[data-testid="sendMessages"').click(); cy.get('[data-testid="sent"]').should('have.text', 'Sent!'); cy.get('@sendMessages').should('be.calledWithExactly', [ { type: 'text', text: 'Hello World', } ]); }); }); SendMessages.spec.js Integration with LINE Platform
  29. describe('Send Messages', () => { it('can send message with mock

    function', () => { cy.stub(liff, 'sendMessages').as('sendMessages').resolves(); cy.stub(liff, 'isLoggedIn').as('isLoggedIn').returns(true); mount(SendMessages); cy.get('[data-testid="sendMessages"').click(); cy.get('[data-testid="sent"]').should('have.text', 'Sent!'); cy.get('@sendMessages').should('be.calledWithExactly', [ { type: 'text', text: 'Hello World', } ]); }); }); SendMessages.spec.js Integration with LINE Platform
  30. Feature vue-test-utils Cypress Running Test in Browser Time Travel Debugger

    Debuggability Via Node Debugger Via Browser DevTools Reporting Text Based Cypress Dashboard + Screenshots + Video Recorded Test Fidelity ⬇ Very Low (virtual DOM) ⬆ Higher + All native browser APIs Interaction with Elements Limited API Use any Cypress Command Mock Library Jest Mock Sinon.js Code Coverage Comparison