Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: write workflow snapshot tests #381

Merged
merged 22 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
bang9 marked this conversation as resolved.
Show resolved Hide resolved
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,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

undefined allows using maximum possible number of workers

/* 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