Skip to content

Commit

Permalink
test: write workflow snapshot tests (#381)
Browse files Browse the repository at this point in the history
## Changes
- N/A

ticket: [AC-4068]

## How to run test & test plan

[link](https://sendbird.atlassian.net/wiki/spaces/AC/pages/2610724994/Widget+snapshot+test+plan)



## Checklist
Before requesting a code review, please check the following:
- [x] **[Required]** CI has passed all checks.
- [ ] **[Required]** A self-review has been conducted to ensure there
are no minor mistakes.
- [ ] **[Required]** Unnecessary comments/debugging code have been
removed.
- [ ] **[Required]** All requirements specified in the ticket have been
accurately implemented.
- [ ] Ensure the ticket has been updated with the sprint, status, and
story points.


[AC-4068]:
https://sendbird.atlassian.net/browse/AC-4068?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

---------

Co-authored-by: Hyungu Kang | Airen <[email protected]>
  • Loading branch information
liamcho and bang9 authored Nov 14, 2024
1 parent 708b278 commit dc69f05
Show file tree
Hide file tree
Showing 43 changed files with 440 additions and 15 deletions.
31 changes: 31 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ executors:
- image: cimg/node:20.13.1
resource_class: medium
working_directory: ~/project
ci-macos:
macos:
xcode: 15.0.0 # Use the closest available version of macOS (Sonoma equivalent may not be available yet)
working_directory: ~/project

commands:
attach_project:
Expand Down Expand Up @@ -105,6 +109,30 @@ jobs:
no_output_timeout: 15m
name: Run test
command: yarn test
# CI - Run snapshot tests
run-snapshot-test:
executor: ci-macos
steps:
- checkout
- run:
name: Enable Corepack
command: corepack enable
- run:
name: Install dependencies
command: |
git submodule update --init --recursive
yarn install --immutable
- run:
name: Install Playwright browsers
command: npx playwright install --with-deps
- run:
name: Run Playwright snapshot tests # refer to https://circleci.com/docs/collect-test-data/#playwright
command: PLAYWRIGHT_JUNIT_OUTPUT_NAME=results.xml yarn playwright test --config=playwright.config.ts
- store_test_results:
path: results.xml
- store_artifacts:
path: ~/project/playwright-report
destination: playwright-html-report

# Publish - build self-service
build:
Expand Down Expand Up @@ -201,6 +229,9 @@ workflows:
- run-test:
requires:
- prepare
ci-snapshot-test:
jobs:
- run-snapshot-test

deploy_prod:
when: << pipeline.parameters.run_deploy_prod >>
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,7 @@ dist-ssr
*.njsproj
*.sln
*.sw?
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions __visual_tests__/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const appId = process.env.SNAPSHOT_TEST_APP_ID;
const botId = process.env.SNAPSHOT_TEST_BOT_ID;

export const TEST_URL = `http://localhost:5173/chat-ai-widget/?app_id=${appId}&bot_id=${botId}&snapshot=true`;

export const WidgetComponentIds = {
WIDGET: '#aichatbot-widget-window',
WIDGET_BUTTON: '#aichatbot-widget-button',
MESSAGE_INPUT: '#sendbird-message-input-text-field',
SUGGESTED_REPLIES_OPTIONS: '.sendbird-suggested-replies__option',
BUTTON: 'button.sendbird-button--primary',
INPUT: '.sendbird-input__input',
CHIPS_CONTAINER: '.sendbird-form-chip__container',
FORM: '#aichatbot-widget-form',
};
31 changes: 31 additions & 0 deletions __visual_tests__/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { expect, Page } from '@playwright/test';

import { WidgetComponentIds } from './const';

export async function assertScreenshot(page: Page, screenshotName: string, browserName: string) {
const name = `${screenshotName}.${browserName}.${process.platform}.png`; // Include the browser and OS architecture info in the filename
await expect(page.locator(WidgetComponentIds.WIDGET)).toHaveScreenshot(name, {
omitBackground: false,
maxDiffPixelRatio: 0.01, // Need this because Sendbird logo is slightly differently rendered in CI.
});
}

export async function loadWidget(page: Page) {
await page.click(WidgetComponentIds.WIDGET_BUTTON);
// NOTE: below fails sometimes in CI.
const widgetWindow = page.locator(WidgetComponentIds.SUGGESTED_REPLIES_OPTIONS);
await widgetWindow.waitFor({ state: 'visible' });
// await page.waitForTimeout(3000);
}

export async function sendTextMessage(page: Page, text: string, waitTime = 1000) {
const input = page.locator(WidgetComponentIds.MESSAGE_INPUT);
await input.fill(text);
await input.press('Enter');
await page.waitForTimeout(waitTime);
}

export async function clickNthChip(page: Page, nth: number) {
const chipContainer = page.locator(WidgetComponentIds.CHIPS_CONTAINER);
await chipContainer.locator(':scope > *').nth(nth).click();
}
129 changes: 129 additions & 0 deletions __visual_tests__/workflow-tests.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { test } from '@playwright/test';

import { TEST_URL, WidgetComponentIds } from './const';
import { assertScreenshot, clickNthChip, loadWidget, sendTextMessage } from './utils';

test.beforeEach(async ({ page }) => {
await page.goto(TEST_URL);
const widgetWindow = page.locator(WidgetComponentIds.WIDGET_BUTTON);
await widgetWindow.waitFor({ state: 'visible' });
});

/**
* 100
* Workflow - Form message
* Steps:
* 1. Send the trigger message: "Give me a food order form"
* 2. Submit form without filling the required fields.
* 3. Submit form with at least one invalid value.
* 4. Submit form with valid values.
*/
test('100', async ({ page, browserName }) => {
await loadWidget(page);

// 1
await sendTextMessage(page, 'Give me a food order form', 0);
const widgetWindow = page.locator(WidgetComponentIds.FORM);
await widgetWindow.waitFor({ state: 'visible' });
await assertScreenshot(page, '100-1', browserName);

// 2
let submitButton = page.locator(WidgetComponentIds.BUTTON);
await submitButton.click();
await assertScreenshot(page, '100-2', browserName);

// 3
const inputs = page.locator(WidgetComponentIds.INPUT);
await inputs.nth(0).fill('guy ordering food');
await inputs.nth(2).fill('not a number');
await inputs.nth(3).fill('not.a.valid.email.com');
await inputs.nth(4).fill('123_456_7890');
await clickNthChip(page, 4);
submitButton = page.locator(WidgetComponentIds.BUTTON);
await page.waitForTimeout(1000);
await assertScreenshot(page, '100-3', browserName);

// 4
await inputs.nth(2).fill('2');
await inputs.nth(3).fill('[email protected]');
await inputs.nth(4).fill('123-456-7890');
await submitButton.click();
await page.waitForTimeout(1000);
await assertScreenshot(page, '100-4', browserName);
});

/**
* 101
* Workflow - Function calls: user message
* Steps:
* 1. Send the trigger message: "Tell me about one cat breed"
*/
test('101', async ({ page, browserName }) => {
await loadWidget(page);
// 1
await sendTextMessage(page, 'Tell me about one cat breed', 2000);
await assertScreenshot(page, '101-1', browserName);
});

/**
* 102
* Workflow - File message
* Steps:
* 1. Send the trigger message: "Give me a travel agency poster"
*/
test('102', async ({ page, browserName }) => {
await loadWidget(page);
// 1
await sendTextMessage(page, 'Give me a travel agency poster', 5000);
await assertScreenshot(page, '102-1', browserName);
});

/**
* 103
* Workflow - Suggested replies with 'Back' enabled
* Steps:
* 1. Send the trigger message: "Suggested replies"
* 2. Click "Text"
* 3. Click "Back"
* 4. Click "File"
* 5. Click "Back"
* 6. Click "Link to workflow: form message"
*/
test('103', async ({ page, browserName }) => {
await loadWidget(page);
// 1
await sendTextMessage(page, 'Suggested replies', 2000);
await assertScreenshot(page, '103-1', browserName);

// 2
let options = page.locator(WidgetComponentIds.SUGGESTED_REPLIES_OPTIONS);
await options.nth(0).click();
await page.waitForTimeout(1000);
await assertScreenshot(page, '103-2', browserName);

// 3
options = page.locator(WidgetComponentIds.SUGGESTED_REPLIES_OPTIONS);
await options.nth(0).click();
await page.waitForTimeout(1000);
await assertScreenshot(page, '103-3', browserName);

// 4
options = page.locator(WidgetComponentIds.SUGGESTED_REPLIES_OPTIONS);
await options.nth(1).click();
await page.waitForTimeout(4000); // Time takes long for file message to be rendered and then scrolled to bottom in CI browsers.
await assertScreenshot(page, '103-4', browserName);

// 5
options = page.locator(WidgetComponentIds.SUGGESTED_REPLIES_OPTIONS);
await options.nth(0).click();
options = page.locator(WidgetComponentIds.SUGGESTED_REPLIES_OPTIONS);
// Expecting three options.
await options.nth(2).waitFor({ state: 'visible' });
await assertScreenshot(page, '103-5', browserName);

// 6
options = page.locator(WidgetComponentIds.SUGGESTED_REPLIES_OPTIONS);
await options.nth(2).click();
await page.waitForTimeout(1000);
await assertScreenshot(page, '103-6', browserName);
});
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@
"build:npm": "node scripts/prebuild.mjs && yarn build",
"build:pages": "rm -rf ./dist && tsc-silent -p './tsconfig.json' --suppress @ && vite build --config vite.config.pages.ts",
"format": "yarn prettier:fix && yarn lint:fix",
"format:check": "yarn prettier src --check && yarn eslint src",
"lint:fix": "yarn eslint src --fix",
"prettier:fix": "yarn prettier src --write",
"format:check": "yarn prettier src __visual_tests__ --check && yarn eslint src __visual_tests__",
"lint:fix": "yarn eslint src __visual_tests__ --fix",
"prettier:fix": "yarn prettier src __visual_tests__ --write",
"preview": "vite preview",
"test": "vitest run"
},
Expand All @@ -37,7 +37,9 @@
"@linaria/atomic": "^6.2.0",
"@linaria/core": "^6.2.0",
"@linaria/react": "^6.2.1",
"@playwright/test": "^1.48.1",
"@types/dompurify": "^3.0.5",
"@types/node": "^22.7.9",
"@types/react": "^18.0.37",
"@types/react-dom": "^18.0.11",
"@types/styled-components": "^5.1.26",
Expand Down
83 changes: 83 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import {defineConfig, devices} from '@playwright/test';

/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') });

/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './__visual_tests__',
snapshotPathTemplate: '{testDir}/__snapshots__/{testFilePath}/{arg}{ext}', // Refer to: https://playwright.dev/docs/next/api/class-testproject#test-project-snapshot-path-template
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: 0, // process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: undefined, // process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: [
['junit', { outputFile: 'results.xml' }],
['html']
],
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',

/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},

/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }, // Note we cannot use ...devices['Desktop Chrome'] because the name varies between devices and in CircleCI environment. CI test will fail because of this.
},

{
name: 'firefox',
use: { ...devices['Desktop Firefox'] }, // Note we cannot use ...devices['Desktop Firefox'] because the name varies between devices and in CircleCI environment. CI test will fail because of this.
},

// {
// name: 'webkit',
// use: { ...devices['Desktop Safari'] },
// },

/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },

/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],

/* Run your local dev server before starting the tests */
webServer: {
command: 'yarn dev',
url: 'http://localhost:5173/chat-ai-widget/',
reuseExistingServer: !process.env.CI,
},
});
12 changes: 9 additions & 3 deletions src/components/BotMessageWithBodyInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const HEIGHTS = {

export default function BotMessageWithBodyInput(props: Props) {
const { botUser } = useChatContext();
const { botStudioEditProps, dateLocale } = useConstantState();
const { botStudioEditProps, dateLocale, stringSet } = useConstantState();

const { createdAt, bodyComponent, chainTop, chainBottom, messageFeedback, wideContainer = false } = props;

Expand Down Expand Up @@ -86,10 +86,16 @@ export default function BotMessageWithBodyInput(props: Props) {
<Content>
{bodyComponent}
{!wideContainer && !!createdAt && (
<DefaultSentTime>{formatCreatedAtToAMPM(createdAt, dateLocale)}</DefaultSentTime>
<DefaultSentTime>
{formatCreatedAtToAMPM(createdAt, stringSet.DATE_FORMAT__MESSAGE_TIMESTAMP, dateLocale)}
</DefaultSentTime>
)}
</Content>
{wideContainer && !!createdAt && <WideSentTime>{formatCreatedAtToAMPM(createdAt, dateLocale)}</WideSentTime>}
{wideContainer && !!createdAt && (
<WideSentTime>
{formatCreatedAtToAMPM(createdAt, stringSet.DATE_FORMAT__MESSAGE_TIMESTAMP, dateLocale)}
</WideSentTime>
)}
{displayProfileImage && messageFeedback}
</FullBodyContainer>
</Root>
Expand Down
Loading

0 comments on commit dc69f05

Please sign in to comment.