From a1a52d4d151608aff3aa8f138a1e48f1cec8cad8 Mon Sep 17 00:00:00 2001 From: Liam Hongman Cho Date: Mon, 25 Nov 2024 13:23:21 +0900 Subject: [PATCH] test: Update snapshot test to cleanup test resources after each test run (#391) ## Changes - N/A ticket: [AC-4343] ## Additional Notes - Updated snapshot test to cleanup test resources after each test run - You can check test resources being created and then deleted in real time during test run - [Run test locally](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. - [x] **[Required]** A self-review has been conducted to ensure there are no minor mistakes. - [x] **[Required]** Unnecessary comments/debugging code have been removed. - [x] **[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-4343]: https://sendbird.atlassian.net/browse/AC-4343?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --------- Co-authored-by: Hyungu Kang | Airen --- __visual_tests__/const.ts | 9 ++-- __visual_tests__/utils/localStorageUtils.ts | 26 +++++++++++ __visual_tests__/utils/requestUtils.ts | 43 +++++++++++++++++++ .../{utils.ts => utils/testUtils.ts} | 21 ++++++++- __visual_tests__/workflow-tests.spec.ts | 18 ++++++-- 5 files changed, 109 insertions(+), 8 deletions(-) create mode 100644 __visual_tests__/utils/localStorageUtils.ts create mode 100644 __visual_tests__/utils/requestUtils.ts rename __visual_tests__/{utils.ts => utils/testUtils.ts} (67%) diff --git a/__visual_tests__/const.ts b/__visual_tests__/const.ts index e43b07633..b8cbde4f5 100644 --- a/__visual_tests__/const.ts +++ b/__visual_tests__/const.ts @@ -1,7 +1,10 @@ -const appId = process.env.SNAPSHOT_TEST_APP_ID; -const botId = process.env.SNAPSHOT_TEST_BOT_ID; +export const AppId = process.env.SNAPSHOT_TEST_APP_ID; +export const BotId = process.env.SNAPSHOT_TEST_BOT_ID; +export const ApiToken = process.env.SNAPSHOT_TEST_API_TOKEN; -export const TEST_URL = `http://localhost:5173/chat-ai-widget/?app_id=${appId}&bot_id=${botId}&snapshot=true`; +export const ApiHost = `https://api-${AppId}.sendbird.com`; + +export const TestUrl = `http://localhost:5173/chat-ai-widget/?app_id=${AppId}&bot_id=${BotId}&snapshot=true`; export const WidgetComponentIds = { WIDGET: '#aichatbot-widget-window', diff --git a/__visual_tests__/utils/localStorageUtils.ts b/__visual_tests__/utils/localStorageUtils.ts new file mode 100644 index 000000000..e553aae1f --- /dev/null +++ b/__visual_tests__/utils/localStorageUtils.ts @@ -0,0 +1,26 @@ +import { Page } from '@playwright/test'; + +export const getKey = (appId: string, botId: string) => { + return `@sendbird/chat-ai-widget/${appId}/${botId}`; +}; + +export type WidgetSessionCache = { + userId: string; + channelUrl: string; +}; + +export async function getWidgetSessionCache( + page: Page, + { appId, botId }: { appId: string; botId: string }, +): Promise { + const value = await page.evaluate(({ key }) => localStorage.getItem(key), { key: getKey(appId, botId) }); + if (value) { + try { + return JSON.parse(value); + } catch { + return null; + } + } else { + return null; + } +} diff --git a/__visual_tests__/utils/requestUtils.ts b/__visual_tests__/utils/requestUtils.ts new file mode 100644 index 000000000..a86ca029b --- /dev/null +++ b/__visual_tests__/utils/requestUtils.ts @@ -0,0 +1,43 @@ +import { ApiHost, ApiToken } from '../const'; + +interface RequestParams { + url: string; + headers?: object; + data?: object; +} + +function createQueryString(params: any): string { + const items: string[] = []; + for (const key in params) { + items.push(`${key}=${encodeURIComponent(params[key])}`); + } + return items.join('&'); +} + +function createHeaders(): object { + return { + 'Api-Token': ApiToken, + 'Content-Type': 'application/json', + }; +} + +async function requestDelete(requestParams: RequestParams) { + const response = await fetch(`${ApiHost}${requestParams.url}?${createQueryString(requestParams.data)}`, { + method: 'DELETE', + headers: createHeaders() as Headers, + body: JSON.stringify(requestParams.data) || null, + }); + return await response.json(); +} + +export async function deleteChannel(channelUrl: string): Promise { + return await requestDelete({ + url: `/v3/group_channels/${encodeURIComponent(channelUrl)}`, + }); +} + +export async function deleteUser(userId: string): Promise { + return await requestDelete({ + url: `/v3/users/${encodeURIComponent(userId)}`, + }); +} diff --git a/__visual_tests__/utils.ts b/__visual_tests__/utils/testUtils.ts similarity index 67% rename from __visual_tests__/utils.ts rename to __visual_tests__/utils/testUtils.ts index 6d6634335..c603f0e12 100644 --- a/__visual_tests__/utils.ts +++ b/__visual_tests__/utils/testUtils.ts @@ -1,6 +1,8 @@ import { expect, Page } from '@playwright/test'; -import { WidgetComponentIds } from './const'; +import { getWidgetSessionCache } from './localStorageUtils'; +import { deleteChannel, deleteUser } from './requestUtils'; +import { AppId, BotId, 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 @@ -29,3 +31,20 @@ export async function clickNthChip(page: Page, nth: number) { const chipContainer = page.locator(WidgetComponentIds.CHIPS_CONTAINER); await chipContainer.locator(':scope > *').nth(nth).click(); } + +export async function deleteTestResources(page: Page) { + if (AppId && BotId) { + const cachedSession = await getWidgetSessionCache(page, { + appId: AppId, + botId: BotId, + }); + if (cachedSession) { + try { + await deleteChannel(cachedSession.channelUrl); + await deleteUser(cachedSession.userId); + } catch (e) { + console.error('## deleteTestResources failed: ', e); + } + } + } +} diff --git a/__visual_tests__/workflow-tests.spec.ts b/__visual_tests__/workflow-tests.spec.ts index ecbcfabda..49778c090 100644 --- a/__visual_tests__/workflow-tests.spec.ts +++ b/__visual_tests__/workflow-tests.spec.ts @@ -1,14 +1,24 @@ import { test } from '@playwright/test'; -import { TEST_URL, WidgetComponentIds } from './const'; -import { assertScreenshot, clickNthChip, loadWidget, sendTextMessage } from './utils'; +import { TestUrl, WidgetComponentIds } from './const'; +import { assertScreenshot, clickNthChip, deleteTestResources, loadWidget, sendTextMessage } from './utils/testUtils'; test.beforeEach(async ({ page }) => { - await page.goto(TEST_URL); + await page.goto(TestUrl); + const widgetWindow = page.locator(WidgetComponentIds.WIDGET_BUTTON); await widgetWindow.waitFor({ state: 'visible' }); }); +test.afterEach(async ({ page }) => { + await deleteTestResources(page); + /** + * Optional: Playwright automatically handles page closure at the end of a test, + * but explicitly closing it ensures no lingering resources remain. + */ + await page.close(); +}); + /** * 100 * Workflow - Form message @@ -124,6 +134,6 @@ test('103', async ({ page, browserName }) => { // 6 options = page.locator(WidgetComponentIds.SUGGESTED_REPLIES_OPTIONS); await options.nth(2).click(); - await page.waitForTimeout(1000); + await page.waitForTimeout(2000); await assertScreenshot(page, '103-6', browserName); });