Slide 1

Slide 1 text

11:15 - 11:50 Traitanit Huangsri LIFF Component Testing with Cypress Senior Software Engineer, LINE Thailand

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

LIFF Component Testing with Cypress

Slide 4

Slide 4 text

About Me • Nott (Traitanit Huangsri) • Software Engineer - LINE Thailand • Cypress Ambassador

Slide 5

Slide 5 text

LIFF 2020 Updates

Slide 6

Slide 6 text

> Mobile & External Browsers Supported > New APIs: shareTargetPicker, ready, getLineVersion etc. > npm install @line/liff // since LIFF 2.3 LIFF 2020 Brief Updates

Slide 7

Slide 7 text

LET’S TALK ABOUT TESTING

Slide 8

Slide 8 text

Let’s find the answer together! How Do You Test Your LIFF App ?

Slide 9

Slide 9 text

UNIT TESTS INTEGRATION TESTS E2E TESTS Testing Pyramid Automated Testing Strategy

Slide 10

Slide 10 text

Testing Pyramid UNIT TESTS INTEGRATION TESTS E2E TESTS Automated Testing Strategy

Slide 11

Slide 11 text

High Fidelity, Low Speed Low Fidelity, Very Fast UNIT TESTS INTEGRATION TESTS E2E TESTS Automated Testing Strategy

Slide 12

Slide 12 text

“It would be great if we have unit tests with Higher FIDELITY and Good SPEED”

Slide 13

Slide 13 text

LET ME Introduce

Slide 14

Slide 14 text

Free & Open Source (MIT License) A tool to quickly and reliably test anything that runs in a browser.

Slide 15

Slide 15 text

Stateless Stateful Why Cypress ?

Slide 16

Slide 16 text

$ npm install -D cypress

Slide 17

Slide 17 text

All-in-one testing tool. Provides popular off-the-shelf Mocha, Sinon and Chai and more bundled test utilities $ npm install -D cypress

Slide 18

Slide 18 text

Your App & Your Tests Same Run Loop Provisioning & Perform system-level tasks How Cypress Works ?

Slide 19

Slide 19 text

Web Frameworks Compatibility

Slide 20

Slide 20 text

Command Log Your App Cypress UI Test Runner

Slide 21

Slide 21 text

Trace Error to your source Human Readable Error Messages Debuggability

Slide 22

Slide 22 text

Time Travel

Slide 23

Slide 23 text

Automatic Wait

Slide 24

Slide 24 text

Realtime Reloads

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

Not only End-to-End Tests But also Unit Tests With full capabilities of Cypress Features

Slide 27

Slide 27 text

LIFF Component Testing with

Slide 28

Slide 28 text

LIFF SDK 1. Dependency with LINE Platform 2. Emulate Opening LIFF from LINE App is so difficult 3. Negative Test ??? LIFF Testing Problems

Slide 29

Slide 29 text

LIFF SDK LIFF Testing Problems

Slide 30

Slide 30 text

> 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

Slide 31

Slide 31 text

Vue.js Cypress LIFF Let’s Get Started

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

Hello {{ name }}!

export default { name: 'Greetings', props: { name: { type: String, required: true } } } Greetings.vue Basic Example

Slide 34

Slide 34 text

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)

Slide 35

Slide 35 text

$ vue-cli-service test:component Component Test with Cypress

Slide 36

Slide 36 text

App.vue created: async function () { try { await liff.init({ liffId: process.env.VUE_APP_LINE_LIFF_ID, }); } catch (error) { this.$swal({ icon: 'error', title: '

LIFF init failed

', html: `${error.message}`, }); } } Scenario 1: Stub LIFF initialization LIFF Component Testing Examples

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

Display stub was called Scenario 1: Stub LIFF initialization Running Component Tests

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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); }); });

Slide 43

Slide 43 text

Scenario 2: LIFF Init Failure Negative Test with LIFF SDK

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

Scenario 3: Test Login with LINE Authentication with LINE Login

Slide 46

Slide 46 text

describe('Profile', () => { beforeEach(() => { Cypress.sinon.replace(liff, 'ready', Promise.resolve()); }); it('render displayName when logged-in', () => { const profile = { displayName: 'Test User', pictureUrl: '' } 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

Slide 47

Slide 47 text

describe('Profile', () => { beforeEach(() => { Cypress.sinon.replace(liff, 'ready', Promise.resolve()); }); it('render displayName when logged-in', () => { const profile = { displayName: 'Test User', pictureUrl: '' } 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

Slide 48

Slide 48 text

describe('Profile', () => { beforeEach(() => { Cypress.sinon.replace(liff, 'ready', Promise.resolve()); }); it('render displayName when logged-in', () => { const profile = { displayName: 'Test User', pictureUrl: '' } 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

Slide 49

Slide 49 text

Scenario 3: Test Login with LINE Authentication with LINE Login

Slide 50

Slide 50 text

Emulate Opening LIFF from LINE App export default { name: 'Mobile', data() { return { isInClient: false, os: '', } }, created() { this.isInClient = liff.isInClient(); this.os = liff.getOS(); } }

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

Scenario 4: Emulate Opening LIFF From LINE App Emulate Opening LIFF from LINE App

Slide 54

Slide 54 text

sendMessages: async function () { if (liff.isLoggedIn()) { await liff.sendMessages([ { type: 'text', text: 'Hello World', } ]); this.$swal({ icon: 'success', title: '

Sent!

', }); } } SendMessages.vue Integration with LINE Platform

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

Scenario 5: Send Message to Chat Integration with LINE Platform

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

Cypress React Unit Test https://github.com/bahmutov/cypress-react-unit-test Cypress Angularjs Unit Test https://github.com/bahmutov/cypress-angular-unit-test Not a Vue.Js Developer?

Slide 61

Slide 61 text

https://fb.com/groups/cypressiothailand Join us Cypress.io Thailand

Slide 62

Slide 62 text

Blog Available Today!

Slide 63

Slide 63 text

No content