Skip to content

Commit

Permalink
Basic setup for chat-widget-adapters e2e tests
Browse files Browse the repository at this point in the history
  • Loading branch information
klarzynskik committed Jan 18, 2022
1 parent 5b13068 commit 6b7ffa1
Show file tree
Hide file tree
Showing 16 changed files with 24,973 additions and 20,750 deletions.
13 changes: 13 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,16 @@ jobs:
run: npm ci
- name: Run test script
run: npm run test
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16.x'

- name: Install dependecies
run: npm ci

- name: Start examples and run e2e tests
run: npm run e2e
5 changes: 4 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ Packages contain a suite of unit tests dedicated for their core functionalities.

#### E2E tests

TODO: Describe E2E tests setup
In order to make sure that the packages are all in sync and their core functionalities are working, we have created a suite of end-to-end tests based on the prepared `examples`. They simply navigate to the URL of each example in the repository and validate the Chat Widget presence and related data.

- To run tests use: `npm run e2e`
- Tests are located in: `e2e`

### Coverage

Expand Down
11 changes: 11 additions & 0 deletions e2e/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": [
"plugin:playwright/playwright-test"
],
"rules": {
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/no-unnecessary-type-assertion": "off"
}
}
89 changes: 89 additions & 0 deletions e2e/example-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { ElementHandle, Frame, Locator, Page } from '@playwright/test'
import { ChatData, WidgetState } from '@livechat/widget-core'
import { matchFrame } from './utils'

export const MAP_FRAMEWORK_TO_PORT = {
React: '3001',
Vue: '3002',
Angular: '3003',
} as const

export class ExamplePage {
readonly widgetIframeId: string = '#chat-widget'
readonly widgetReadiness: Locator
readonly dataContainers: {
chatData: Locator
customerData: Locator
widgetIsReady: Locator
widgetState: Locator
}
preChat: {
emailInput: ElementHandle<SVGElement | HTMLElement>
nameInput: ElementHandle<SVGElement | HTMLElement>
}
sentFrames: string[] = []
receivedFrames: string[] = []
serverCustomerId: null | string = null
serverChat: null | ChatData = null
widgetFrame: Frame
widgetState: WidgetState

constructor(private page: Page, private framework: keyof typeof MAP_FRAMEWORK_TO_PORT) {
this.dataContainers = {
chatData: page.locator('#chat-data'),
customerData: page.locator('#customer-data'),
widgetIsReady: page.locator('#widget-readiness'),
widgetState: page.locator('#widget-state'),
}
}

async goto() {
this.page.on('websocket', (ws) => {
ws.on('framesent', (event) => this.sentFrames.push(event.payload as string))
ws.on('framereceived', (event) => this.receivedFrames.push(event.payload as string))
})

await this.page.goto(`http://localhost:${MAP_FRAMEWORK_TO_PORT[this.framework]}`)
await this.page.waitForSelector(`text=Hello ${this.framework}!`, { state: 'visible' })
}

async getWidgetIframe() {
await this.page.waitForSelector(this.widgetIframeId)

const elementHandle = await this.page.$(this.widgetIframeId)
this.widgetFrame = await elementHandle.contentFrame()
}

async startTheChat() {
const startTheChatButton = await this.widgetFrame.waitForSelector('text=Start the chat', {
state: 'visible',
})

await startTheChatButton.click()

await this.widgetFrame.waitForSelector('text=Hello. How may I help you?', {
state: 'visible',
})

const serverChat = matchFrame(this.receivedFrames, new RegExp(/incoming_chat/)).payload.chat
this.serverChat = {
chatId: serverChat.id,
threadId: serverChat.thread.id,
}
}

async minimizeWidget() {
await this.widgetFrame.locator('[aria-label="Minimize window"]').click()
}

getPreChatFields() {
return {
emailInput: this.widgetFrame.locator('#email'),
nameInput: this.widgetFrame.locator('#name'),
}
}

getServerCustomerId() {
return matchFrame(this.receivedFrames, new RegExp(/login/)).payload.customer_id
}
}
50 changes: 50 additions & 0 deletions e2e/examples.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { expect, test } from '@playwright/test'
import { CustomerData, ChatData, WidgetState } from '@livechat/widget-core'
import { ExamplePage, MAP_FRAMEWORK_TO_PORT } from './example-page'
import { stringify } from './utils'

test.describe.parallel('Example applications', () => {
Object.keys(MAP_FRAMEWORK_TO_PORT).forEach((framework: keyof typeof MAP_FRAMEWORK_TO_PORT) => {
test(`${framework} should render Chat Widget with provided data and update state accordingly`, async ({ page }) => {
const customerData: CustomerData = {
name: 'John Doe',
email: 'john.doe@example.com',
isReturning: false,
sessionVariables: {},
id: null,
status: 'browsing',
}

const widgetState: WidgetState = {
availability: 'online',
visibility: 'maximized',
}

let chatData: ChatData = null

const examplePage = new ExamplePage(page, framework)
await examplePage.goto()
await examplePage.getWidgetIframe()

customerData.id = examplePage.getServerCustomerId()
await expect(examplePage.dataContainers.widgetIsReady).toHaveText('Widget is ready: true')
await expect(examplePage.dataContainers.widgetState).toHaveText(`Widget state: ${stringify(widgetState)}`)
await expect(examplePage.dataContainers.customerData).toHaveText(`Customer data: ${stringify(customerData)}`)
await expect(examplePage.dataContainers.chatData).toHaveText(`Chat data: ${stringify(chatData)}`)

const preChatFields = examplePage.getPreChatFields()
await expect(preChatFields.emailInput).toHaveValue(customerData.email)
await expect(preChatFields.nameInput).toHaveValue(customerData.name)

await examplePage.startTheChat()
customerData.status = 'chatting'
chatData = examplePage.serverChat
await expect(examplePage.dataContainers.chatData).toHaveText(`Chat data: ${stringify(chatData)}`)
await expect(examplePage.dataContainers.customerData).toHaveText(`Customer data: ${stringify(customerData)}`)

await examplePage.minimizeWidget()
widgetState.visibility = 'minimized'
await expect(examplePage.dataContainers.widgetState).toHaveText(`Widget state: ${stringify(widgetState)}`)
})
})
})
23 changes: 23 additions & 0 deletions e2e/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { PlaywrightTestConfig, devices } from '@playwright/test'

const config: PlaywrightTestConfig = {
testDir: '.',
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
use: {
trace: 'on-first-retry',
},
webServer: {
command: 'npm start examples',
port: 3003,
timeout: 120 * 1000,
reuseExistingServer: !process.env.CI,
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
}
export default config
7 changes: 7 additions & 0 deletions e2e/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function stringify(value: Parameters<typeof JSON.stringify>['0']) {
return JSON.stringify(value, null, 2)
}

export function matchFrame(arr: string[], matcher: RegExp): Record<any, any> {
return JSON.parse(arr.find((element) => element.match(matcher)))
}
10 changes: 5 additions & 5 deletions examples/angular/src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<main>
<h1>Hello Angular!</h1>
<button type="button" [disabled]="!(widgetIsReady$ | async)" (click)="handleChangeGroup()">Change group</button>
<pre>Widget is ready: {{ widgetIsReady$ | async }}</pre>
<pre>Widget state: {{ widgetState$ | async | json }}</pre>
<pre>Customer data: {{ customerData$ | async | json }}</pre>
<pre>Chat data: {{ chatData$ | async | json }}</pre>
<pre>Greeting: {{ greeting$ | async | json }}</pre>
<pre id="widget-readiness">Widget is ready: {{ widgetIsReady$ | async }}</pre>
<pre id="widget-state">Widget state: {{ widgetState$ | async | json }}</pre>
<pre id="customer-data">Customer data: {{ customerData$ | async | json }}</pre>
<pre id="chat-data">Chat data: {{ chatData$ | async | json }}</pre>
<pre id="greeting">Greeting: {{ greeting$ | async | json }}</pre>

<livechat-widget
license="12332502"
Expand Down
12 changes: 6 additions & 6 deletions examples/react/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,17 @@ export function App() {
>
Change group
</button>
<pre>Widget is ready: {stringify(isWidgetReady)}</pre>
<pre>Widget state: {stringify(widgetState)}</pre>
<pre>Customer data: {stringify(customerData)}</pre>
<pre>Chat data: {stringify(chatData)}</pre>
<pre>Greeting: {stringify(greeting)}</pre>
<pre id="widget-readiness">Widget is ready: {stringify(isWidgetReady)}</pre>
<pre id="widget-state">Widget state: {stringify(widgetState)}</pre>
<pre id="customer-data">Customer data: {stringify(customerData)}</pre>
<pre id="chat-data">Chat data: {stringify(chatData)}</pre>
<pre id="greeting">Greeting: {stringify(greeting)}</pre>
<LiveChatWidget
license="12332502"
group={group}
visibility="maximized"
customerName="John Doe"
customerEmail="joh.doe@example.com"
customerEmail="john.doe@example.com"
onNewEvent={(event) => console.log('LiveChatWidget -> onNewEvent', stringify(event))}
onFormSubmitted={(form) => console.log('LiveChatWidget -> onFormSubmitted', stringify(form))}
onRatingSubmitted={(rating) => console.log('LiveChatWidget -> onRatingSubmitted', stringify(rating))}
Expand Down
12 changes: 6 additions & 6 deletions examples/vue/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@
<main>
<h1>Hello Vue!</h1>
<button type="button" :disabled="!isWidgetReady" v-on:click="handleChangeGroup">Change group</button>
<pre>Widget is ready: {{stringify(isWidgetReady)}}</pre>
<pre>Widget state: {{stringify(widgetState)}}</pre>
<pre>Customer data: {{stringify(customerData)}}</pre>
<pre>Chat data: {{stringify(chatData)}}</pre>
<pre>Greeting: {{stringify(greeting)}}</pre>
<pre id="widget-readiness">Widget is ready: {{stringify(isWidgetReady)}}</pre>
<pre id="widget-state">Widget state: {{stringify(widgetState)}}</pre>
<pre id="customer-data">Customer data: {{stringify(customerData)}}</pre>
<pre id="chat-data">Chat data: {{stringify(chatData)}}</pre>
<pre id="greeting">Greeting: {{stringify(greeting)}}</pre>
<LiveChatWidget
license="12332502"
:group="group"
visibility="maximized"
customerName="John Doe"
customerEmail="joh.doe@example.com"
customerEmail="john.doe@example.com"
v-on:new-event="handleNewEvent"
v-on:form-submitted="handleFormSubmitted"
v-on:rating-submitted="handleRatingSubmitted"
Expand Down
Loading

0 comments on commit 6b7ffa1

Please sign in to comment.