first commit
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
import { expect, test as setup } from '@playwright/test';
|
||||
|
||||
import { initializeDatabaseForTest } from './utils/database-utils';
|
||||
import { initializeOpenSearchForTest } from './utils/opensearch-utils';
|
||||
|
||||
const authFile = 'playwright/.auth/user.json';
|
||||
|
||||
setup('tenant create and authenticate', async ({ page }) => {
|
||||
try {
|
||||
await initializeDatabaseForTest();
|
||||
console.log('Database initialized for test');
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'Database initialization failed, continuing without database cleanup:',
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await initializeOpenSearchForTest();
|
||||
console.log('OpenSearch initialized for test');
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'OpenSearch initialization failed, continuing without OpenSearch:',
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
await page.goto('http://localhost:3000/tenant/create', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.locator("input[name='siteName']").click();
|
||||
await page.locator("input[name='siteName']").fill('TestTenant');
|
||||
await page.getByRole('button', { name: 'Next', exact: true }).click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.locator("input[name='email']").click();
|
||||
await page.locator("input[name='email']").fill('sdi9429@naver.com');
|
||||
|
||||
await page.getByRole('button', { name: 'Request Code', exact: true }).click();
|
||||
|
||||
await page.waitForSelector('input[name="code"]', {
|
||||
state: 'visible',
|
||||
timeout: 60000,
|
||||
});
|
||||
await page.locator("input[name='code']").click();
|
||||
await page.locator("input[name='code']").fill('000000');
|
||||
|
||||
await page.getByRole('button', { name: 'Verify Code', exact: true }).click();
|
||||
|
||||
await page.locator("input[name='password']").click();
|
||||
await page.locator("input[name='password']").fill('Abcd1234!');
|
||||
|
||||
await page.locator("input[name='confirmPassword']").click();
|
||||
await page.locator("input[name='confirmPassword']").fill('Abcd1234!');
|
||||
|
||||
await page.getByRole('button', { name: 'Next', exact: true }).click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.getByRole('button', { name: 'Confirm', exact: true }).click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.goto('http://localhost:3000/auth/sign-in', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await expect(page.locator('body', { hasText: 'TestTenant' })).toContainText(
|
||||
'TestTenant',
|
||||
);
|
||||
|
||||
await page.locator("input[name='email']").click();
|
||||
await page.locator("input[name='email']").fill('sdi9429@naver.com');
|
||||
await page.locator("input[name='password']").click();
|
||||
await page.locator("input[name='password']").fill('Abcd1234!');
|
||||
await page.getByRole('button', { name: 'Sign In', exact: true }).click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.waitForURL('http://localhost:3000/main/project/create');
|
||||
|
||||
await page.context().storageState({ path: authFile });
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { test as teardown } from '@playwright/test';
|
||||
|
||||
import { cleanupDatabaseAfterTest } from './utils/database-utils';
|
||||
import { cleanupOpenSearchAfterTest } from './utils/opensearch-utils';
|
||||
|
||||
teardown('teardown', async () => {
|
||||
try {
|
||||
try {
|
||||
await cleanupDatabaseAfterTest();
|
||||
} catch (error) {
|
||||
console.warn('Database cleanup failed:', error);
|
||||
}
|
||||
|
||||
try {
|
||||
await cleanupOpenSearchAfterTest();
|
||||
} catch (error) {
|
||||
console.warn('OpenSearch cleanup failed:', error);
|
||||
}
|
||||
|
||||
console.log('Tearing down succeeds.');
|
||||
} catch (e) {
|
||||
console.log('Tearing down fails.', e);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "e2e",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"clean": "git clean -xdf node_modules playwright-report test-results",
|
||||
"test:e2e": "playwright test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@opensearch-project/opensearch": "^3.5.1",
|
||||
"@playwright/test": "^1.58.2",
|
||||
"axios": "^1.13.6",
|
||||
"mysql2": "^3.20.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import * as path from 'path';
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
import 'dotenv/config';
|
||||
|
||||
export const STORAGE_STATE = path.join(__dirname, 'playwright/.auth/user.json');
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
|
||||
export default defineConfig({
|
||||
testDir: '.',
|
||||
/* Maximum time one test can run for. */
|
||||
timeout: 60 * 1000,
|
||||
expect: {
|
||||
/**
|
||||
* Maximum time expect() should wait for the condition to be met.
|
||||
* For example in `await expect(locator).toHaveText();`
|
||||
*/
|
||||
timeout: 60 * 1000,
|
||||
},
|
||||
/* 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: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: 1,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
testMatch: 'test.list.*',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
|
||||
actionTimeout: 0,
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
// baseURL: 'http://localhost:3000',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'retain-on-failure',
|
||||
video: 'on',
|
||||
screenshot: 'only-on-failure',
|
||||
},
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'global setup',
|
||||
testMatch: /global\.setup\.ts/,
|
||||
teardown: 'global teardown',
|
||||
},
|
||||
{
|
||||
name: 'global teardown',
|
||||
testMatch: /global\.teardown\.ts/,
|
||||
},
|
||||
{
|
||||
name: 'logged in chromium',
|
||||
use: { ...devices['Desktop Chrome'], storageState: STORAGE_STATE },
|
||||
dependencies: ['global setup'],
|
||||
},
|
||||
// {
|
||||
// name: 'logged out chromium',
|
||||
// testMatch: '**/sign-in*.spec.ts',
|
||||
// use: { ...devices['Desktop Chrome'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'firefox',
|
||||
// use: { ...devices['Desktop Firefox'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'webkit',
|
||||
// use: { ...devices['Desktop Safari'] },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
|
||||
outputDir: 'test-results/',
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: [
|
||||
{
|
||||
command:
|
||||
!process.env.CI ?
|
||||
'cd ../.. && pnpm dev:api'
|
||||
: 'cd ../.. && pnpm build:api && cd apps/api && pnpm start',
|
||||
port: 4000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 120 * 1000,
|
||||
env: {
|
||||
JWT_SECRET: 'jwtsecretjwtsecretjwtsecret',
|
||||
BASE_URL: 'http://localhost:3000',
|
||||
MYSQL_PRIMARY_URL:
|
||||
'mysql://userfeedback:userfeedback@localhost:13307/e2e',
|
||||
MYSQL_SECONDARY_URLS:
|
||||
'["mysql://userfeedback:userfeedback@localhost:13307/e2e"]',
|
||||
AUTO_MIGRATION: 'true',
|
||||
MASTER_API_KEY: 'MASTER_API_KEY',
|
||||
NODE_ENV: 'test',
|
||||
SMTP_HOST: 'localhost',
|
||||
SMTP_PORT: '25',
|
||||
SMTP_SENDER: 'abc@feedback.user',
|
||||
OPENSEARCH_USE: 'true',
|
||||
OPENSEARCH_NODE: 'http://localhost:9200',
|
||||
OPENSEARCH_USERNAME: '',
|
||||
OPENSEARCH_PASSWORD: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
command:
|
||||
!process.env.CI ?
|
||||
'cd ../.. && pnpm dev:web'
|
||||
: 'cd ../.. && pnpm build:web && cd apps/web && pnpm start',
|
||||
port: 3000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 120 * 1000,
|
||||
env: {
|
||||
NEXT_PUBLIC_API_BASE_URL: 'http://localhost:4000',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
export default () => {
|
||||
test.describe('create-channel suite', () => {
|
||||
test('creating a channel succeeds', async ({ page }) => {
|
||||
await page.goto('http://localhost:3000', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.getByRole('radio', { name: 'Settings' }).click();
|
||||
await page.waitForTimeout(1000);
|
||||
await page.getByText('Create Channel').click();
|
||||
await page.waitForTimeout(1000);
|
||||
await page.locator('input[name="name"]').click();
|
||||
await page.locator('input[name="name"]').fill('TestChannel');
|
||||
await page.getByRole('button', { name: 'Next' }).click();
|
||||
|
||||
await page.getByText('Add Field').first().click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.waitForSelector('input[name="key"]', { state: 'visible' });
|
||||
await page.locator('input[name="key"]').click();
|
||||
await page.locator('input[name="key"]').fill('message');
|
||||
await page.getByRole('button', { name: 'Confirm' }).click();
|
||||
|
||||
await page.getByText('Add Field').first().click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.waitForSelector('input[name="key"]', { state: 'visible' });
|
||||
await page.locator('input[name="key"]').click();
|
||||
await page.locator('input[name="key"]').fill('image');
|
||||
|
||||
await page
|
||||
.locator('select[aria-hidden="true"]')
|
||||
.first()
|
||||
.selectOption('images');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.getByRole('button', { name: 'Confirm' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Complete' }).click();
|
||||
await page.waitForTimeout(1000);
|
||||
await page.getByRole('button', { name: 'Start' }).click();
|
||||
await page.waitForTimeout(1000);
|
||||
await expect(
|
||||
page.getByRole('tab', { name: 'TestChannel' }).first(),
|
||||
).toContainText('TestChannel');
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import axios from 'axios';
|
||||
|
||||
export default () => {
|
||||
test.describe('create-feedback-with-image suite', () => {
|
||||
test.afterEach(async ({ page }) => {
|
||||
await page.getByText('test text').first().click();
|
||||
await page.waitForTimeout(1000);
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
|
||||
await expect(page.getByText('Delete').nth(3)).toBeVisible();
|
||||
await page.getByText('Delete').nth(3).click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await expect(page.locator('tbody')).not.toContainText('test text');
|
||||
});
|
||||
|
||||
test('creating a feedback with image succeeds', async ({ page }) => {
|
||||
await page.goto('http://localhost:3000', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
|
||||
await page.getByRole('radio', { name: 'Feedback' }).click();
|
||||
await page.waitForURL(/.*channelId.*/, { timeout: 10000 });
|
||||
|
||||
await expect(page.getByText('There is no data yet')).toBeVisible();
|
||||
|
||||
const url = new URL(page.url());
|
||||
const pathname = url.pathname;
|
||||
const segments = pathname.split('/');
|
||||
const projectId = segments[3];
|
||||
const params = new URLSearchParams(url.search);
|
||||
const channelId = params.get('channelId');
|
||||
|
||||
await axios.post(
|
||||
`http://localhost:4000/api/projects/${projectId}/channels/${channelId}/feedbacks`,
|
||||
{
|
||||
message: 'test text',
|
||||
image: [
|
||||
'https://lps-editor-2050.landpress.line.me/logo/logo_line_color.svg',
|
||||
],
|
||||
},
|
||||
{ headers: { 'x-api-key': 'MASTER_API_KEY' } },
|
||||
);
|
||||
|
||||
await page.goto(
|
||||
`http://localhost:3000/main/project/${projectId}/feedback?channelId=${channelId}`,
|
||||
{ waitUntil: 'domcontentloaded' },
|
||||
);
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.getByText('Image').nth(1).click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.getByText('Cancel').nth(1).click();
|
||||
await page.waitForTimeout(1000);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import axios from 'axios';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export default () => {
|
||||
test.describe('create-feedback suite', () => {
|
||||
test.afterEach(async ({ page }) => {
|
||||
await page.getByText('test text').first().click();
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
|
||||
await expect(page.getByText('Delete').nth(3)).toBeVisible();
|
||||
await page.getByText('Delete').nth(3).click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await expect(page.locator('tbody')).not.toContainText('test text');
|
||||
});
|
||||
|
||||
test('creating a feedback succeeds', async ({ page }) => {
|
||||
await page.goto('http://localhost:3000', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
|
||||
await page.getByRole('radio', { name: 'Feedback' }).click();
|
||||
await page.waitForURL(/.*channelId.*/, { timeout: 10000 });
|
||||
|
||||
await expect(page.getByText('There is no data yet')).toBeVisible();
|
||||
|
||||
const url = new URL(page.url());
|
||||
const pathname = url.pathname;
|
||||
const segments = pathname.split('/');
|
||||
const projectId = segments[3];
|
||||
const params = new URLSearchParams(url.search);
|
||||
const channelId = params.get('channelId');
|
||||
|
||||
await axios.post(
|
||||
`http://localhost:4000/api/projects/${projectId}/channels/${channelId}/feedbacks`,
|
||||
{
|
||||
message: 'test text',
|
||||
},
|
||||
{ headers: { 'x-api-key': 'MASTER_API_KEY' } },
|
||||
);
|
||||
|
||||
await page.goto(
|
||||
`http://localhost:3000/main/project/${projectId}/feedback?channelId=${channelId}`,
|
||||
{ waitUntil: 'domcontentloaded' },
|
||||
);
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await expect(page.locator('tbody')).toContainText('test text');
|
||||
|
||||
let dateSelector = page.getByText(
|
||||
`${dayjs().subtract(364, 'day').format('YYYY-MM-DD')} ~ ${dayjs().format('YYYY-MM-DD')}`,
|
||||
);
|
||||
|
||||
await expect(dateSelector).toBeVisible();
|
||||
await dateSelector.click();
|
||||
await page.getByText('Yesterday').click();
|
||||
await page.getByText('Save').click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await expect(page.getByText('There is no data yet')).toBeVisible();
|
||||
|
||||
dateSelector = page.getByText(
|
||||
`${dayjs().subtract(1, 'day').format('YYYY-MM-DD')} ~ ${dayjs().subtract(1, 'day').format('YYYY-MM-DD')}`,
|
||||
);
|
||||
await dateSelector.click();
|
||||
await page.getByText('Today').click();
|
||||
await page.getByText('Save').click();
|
||||
await page.waitForTimeout(1000);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import axios from 'axios';
|
||||
|
||||
export default () => {
|
||||
test.describe('create-issue suite', () => {
|
||||
test.afterEach(async ({ page }) => {
|
||||
await page
|
||||
.getByRole('button', { name: 'Delete' })
|
||||
.click({ timeout: 1000 });
|
||||
|
||||
await expect(page.getByText('Delete').nth(3)).toBeVisible();
|
||||
await page.getByText('Delete').nth(3).click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await expect(page.locator('tbody')).not.toContainText('test text');
|
||||
});
|
||||
|
||||
test('creating an issue and attaching it to a feedback succeeds', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto('http://localhost:3000', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
|
||||
await page.getByRole('radio', { name: 'Issue' }).click();
|
||||
await page.getByRole('button', { name: 'Create Issue' }).click();
|
||||
await page.waitForTimeout(1000);
|
||||
await page.getByPlaceholder('Please enter.').first().fill('test_issue');
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.getByRole('radio', { name: 'Feedback' }).click();
|
||||
await page.waitForURL(/.*channelId.*/, { timeout: 1000 });
|
||||
|
||||
await expect(page.getByText('There is no data yet')).toBeVisible();
|
||||
|
||||
const url = new URL(page.url());
|
||||
const pathname = url.pathname;
|
||||
const segments = pathname.split('/');
|
||||
const projectId = segments[3];
|
||||
const params = new URLSearchParams(url.search);
|
||||
const channelId = params.get('channelId');
|
||||
|
||||
await axios.post(
|
||||
`http://localhost:4000/api/projects/${projectId}/channels/${channelId}/feedbacks`,
|
||||
{
|
||||
message: 'test text',
|
||||
},
|
||||
{ headers: { 'x-api-key': 'MASTER_API_KEY' } },
|
||||
);
|
||||
|
||||
await page.goto(
|
||||
`http://localhost:3000/main/project/${projectId}/feedback?channelId=${channelId}`,
|
||||
{ waitUntil: 'domcontentloaded' },
|
||||
);
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await expect(page.locator('tbody')).toContainText('test text');
|
||||
|
||||
await page.getByRole('button', { name: '+' }).first().click();
|
||||
const testIssue = page.getByText('test_issue').first();
|
||||
await expect(testIssue).toBeVisible();
|
||||
await testIssue.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.getByRole('button', { name: '+' }).first().click();
|
||||
|
||||
await page.getByText('test text').first().click();
|
||||
|
||||
await page.getByRole('button', { name: '+' }).first().click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.keyboard.insertText('test_issue2');
|
||||
|
||||
await page.getByText('Create', { exact: true }).click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForTimeout(1000);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import axios from 'axios';
|
||||
|
||||
const NUMBER_OF_FEEDBACKS = 25;
|
||||
|
||||
export default () => {
|
||||
test.describe('create-multiple-feedbacks suite', () => {
|
||||
test.afterEach(async ({ page }) => {
|
||||
await page.getByRole('checkbox').nth(1).click();
|
||||
|
||||
await page.getByText('Delete Feedback').click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.getByRole('checkbox').first().click();
|
||||
|
||||
await page.getByText('Delete Feedback').click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
await page.waitForTimeout(1000);
|
||||
});
|
||||
|
||||
test('creating multiple feedbacks succeeds', async ({ page }) => {
|
||||
await page.goto('http://localhost:3000', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
|
||||
await page.getByRole('radio', { name: 'Feedback' }).click();
|
||||
await page.waitForURL(/.*channelId.*/, { timeout: 10000 });
|
||||
|
||||
await expect(page.getByText('There is no data yet')).toBeVisible();
|
||||
|
||||
const url = new URL(page.url());
|
||||
const pathname = url.pathname;
|
||||
const segments = pathname.split('/');
|
||||
const projectId = segments[3];
|
||||
const params = new URLSearchParams(url.search);
|
||||
const channelId = params.get('channelId');
|
||||
|
||||
for (let i = 0; i < NUMBER_OF_FEEDBACKS; i++) {
|
||||
await axios.post(
|
||||
`http://localhost:4000/api/projects/${projectId}/channels/${channelId}/feedbacks`,
|
||||
{
|
||||
message: `test text ${i}`,
|
||||
},
|
||||
{ headers: { 'x-api-key': 'MASTER_API_KEY' } },
|
||||
);
|
||||
}
|
||||
|
||||
await page.goto(
|
||||
`http://localhost:3000/main/project/${projectId}/feedback?channelId=${channelId}`,
|
||||
{ waitUntil: 'domcontentloaded' },
|
||||
);
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await expect(page.locator('tbody')).toContainText('test text');
|
||||
|
||||
await expect(page.getByText('Page 1 of 2')).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: '20', exact: true }).click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.getByRole('menuitem', { name: '30', exact: true }).click();
|
||||
await page.waitForTimeout(1000);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
export default () => {
|
||||
test('creating a project succeeds', async ({ page }) => {
|
||||
await page.goto('http://localhost:3000/main/project/create', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Next' })).toBeVisible({
|
||||
timeout: 1000,
|
||||
});
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.getByRole('button', { name: 'Next' }).click();
|
||||
await page.getByRole('button', { name: 'Next' }).click();
|
||||
await page.getByRole('button', { name: 'Confirm' }).click();
|
||||
|
||||
await page.locator("input[name='name']").click();
|
||||
await page.locator("input[name='name']").fill('TestProject');
|
||||
|
||||
await page.getByRole('button', { name: 'Next' }).click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.getByRole('button', { name: 'Next' }).click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.getByRole('button', { name: 'Complete' }).click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await expect(
|
||||
page.getByRole('heading', {
|
||||
level: 2,
|
||||
name: /project creation complete/i,
|
||||
}),
|
||||
).toBeVisible();
|
||||
await expect(page.locator("input[name='name']")).toHaveValue('TestProject');
|
||||
await page.getByRole('button', { name: 'Later' }).click();
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import axios from 'axios';
|
||||
|
||||
export default () => {
|
||||
test.describe('search-feedback suite', () => {
|
||||
test('creating and searching a feedback succeed', async ({ page }) => {
|
||||
await page.goto('http://localhost:3000', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
|
||||
await page.getByRole('radio', { name: 'Feedback' }).click();
|
||||
await page.waitForURL(/.*channelId.*/, { timeout: 10000 });
|
||||
|
||||
await expect(page.getByText('There is no data yet')).toBeVisible();
|
||||
|
||||
const url = new URL(page.url());
|
||||
const pathname = url.pathname;
|
||||
const segments = pathname.split('/');
|
||||
const projectId = segments[3];
|
||||
const params = new URLSearchParams(url.search);
|
||||
const channelId = params.get('channelId');
|
||||
|
||||
await axios.post(
|
||||
`http://localhost:4000/api/projects/${projectId}/channels/${channelId}/feedbacks`,
|
||||
{
|
||||
message: 'test text1',
|
||||
},
|
||||
{ headers: { 'x-api-key': 'MASTER_API_KEY' } },
|
||||
);
|
||||
|
||||
await axios.post(
|
||||
`http://localhost:4000/api/projects/${projectId}/channels/${channelId}/feedbacks`,
|
||||
{
|
||||
message: 'test text2',
|
||||
},
|
||||
{ headers: { 'x-api-key': 'MASTER_API_KEY' } },
|
||||
);
|
||||
|
||||
await page.goto(
|
||||
`http://localhost:3000/main/project/${projectId}/feedback?channelId=${channelId}`,
|
||||
{ waitUntil: 'domcontentloaded' },
|
||||
);
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await expect(page.locator('tbody')).toContainText('test text1');
|
||||
await expect(page.locator('tbody')).toContainText('test text2');
|
||||
|
||||
await page.getByText('Filter').click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.getByRole('button', { name: 'ID' }).click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.getByRole('option', { name: 'message' }).click();
|
||||
|
||||
await page.getByPlaceholder('Please enter').fill('test text1');
|
||||
|
||||
await page.getByRole('button', { name: 'Confirm' }).click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await expect(page.locator('tbody')).toContainText('test text1');
|
||||
await expect(page.locator('tbody')).not.toContainText('test text2');
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.getByText('Filter').click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.getByText('Add Filter').click();
|
||||
|
||||
await page.getByRole('button', { name: 'ID' }).click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.getByRole('option', { name: 'message' }).click();
|
||||
|
||||
await page.getByPlaceholder('Please enter').nth(1).fill('test text2');
|
||||
|
||||
await page.getByText('And', { exact: true }).click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.getByRole('option', { name: 'Or', exact: true }).click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.getByRole('button', { name: 'Confirm' }).click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await expect(page.locator('tbody')).toContainText('test text1');
|
||||
await expect(page.locator('tbody')).toContainText('test text2');
|
||||
|
||||
await page.getByText('View').click();
|
||||
await page.getByText('ID').nth(1).click();
|
||||
await page.getByText('Created').nth(1).click();
|
||||
await page.getByText('Updated').nth(1).click();
|
||||
|
||||
await page.getByText('Expand').first().click();
|
||||
await page.waitForTimeout(1000);
|
||||
await page.getByText('Expand').first().click();
|
||||
await page.waitForTimeout(1000);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import { test } from '@playwright/test';
|
||||
|
||||
import createChannel from './scenarios/create-channel.spec';
|
||||
import createFeedbackWithImage from './scenarios/create-feedback-with-image.spec';
|
||||
import createFeedback from './scenarios/create-feedback.spec';
|
||||
import createIssue from './scenarios/create-issue.spec';
|
||||
import createMultipleFeedbacks from './scenarios/create-multiple-feedbacks.spec';
|
||||
import createProject from './scenarios/create-project.spec';
|
||||
import searchFeedback from './scenarios/search-feedback.spec';
|
||||
|
||||
test.describe('test suites', () => {
|
||||
createProject();
|
||||
createChannel();
|
||||
createFeedback();
|
||||
createFeedbackWithImage();
|
||||
createMultipleFeedbacks();
|
||||
createIssue();
|
||||
searchFeedback();
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import mysql from 'mysql2/promise';
|
||||
|
||||
export async function createConnection() {
|
||||
return await mysql.createConnection({
|
||||
host: '127.0.0.1',
|
||||
port: 13307,
|
||||
user: 'userfeedback',
|
||||
password: 'userfeedback',
|
||||
database: 'e2e',
|
||||
});
|
||||
}
|
||||
|
||||
export async function waitForDatabase(
|
||||
maxRetries = 30,
|
||||
delayMs = 1000,
|
||||
): Promise<void> {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
const connection = await createConnection();
|
||||
await connection.ping();
|
||||
await connection.end();
|
||||
console.log('Database is ready');
|
||||
return;
|
||||
} catch (error) {
|
||||
console.log(`Waiting for database... (${i + 1}/${maxRetries})`);
|
||||
if (i === maxRetries - 1) {
|
||||
throw new Error(
|
||||
`Database is not available after ${maxRetries} retries`,
|
||||
);
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearDatabaseTables(): Promise<void> {
|
||||
const connection = await createConnection();
|
||||
|
||||
try {
|
||||
await connection.execute('SET FOREIGN_KEY_CHECKS = 0');
|
||||
|
||||
const [tables] = (await connection.execute(
|
||||
"SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'e2e' AND TABLE_TYPE = 'BASE TABLE'",
|
||||
)) as [mysql.RowDataPacket[], mysql.FieldPacket[]];
|
||||
|
||||
for (const table of tables) {
|
||||
const tableName = table.TABLE_NAME;
|
||||
if (tableName !== 'migrations') {
|
||||
console.log(`Clearing table: ${tableName}`);
|
||||
await connection.execute(`DELETE FROM \`${tableName}\``);
|
||||
await connection.execute(
|
||||
`ALTER TABLE \`${tableName}\` AUTO_INCREMENT = 1`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await connection.execute('SET FOREIGN_KEY_CHECKS = 1');
|
||||
|
||||
console.log('Database tables cleared');
|
||||
} catch (error) {
|
||||
console.log('Error clearing database tables:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
await connection.end();
|
||||
}
|
||||
}
|
||||
|
||||
export async function initializeDatabaseForTest(): Promise<void> {
|
||||
try {
|
||||
await waitForDatabase();
|
||||
await clearDatabaseTables();
|
||||
console.log('Database initialized for test');
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize database:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function cleanupDatabaseAfterTest(): Promise<void> {
|
||||
try {
|
||||
await clearDatabaseTables();
|
||||
console.log('Database cleaned up after test');
|
||||
} catch (error) {
|
||||
console.error('Failed to cleanup database:', error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { Client } from '@opensearch-project/opensearch';
|
||||
|
||||
export interface OpenSearchConfig {
|
||||
node: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export async function createOpenSearchClient(
|
||||
config: OpenSearchConfig,
|
||||
): Promise<Client> {
|
||||
const clientConfig: any = {
|
||||
node: config.node,
|
||||
};
|
||||
|
||||
if (config.username && config.password) {
|
||||
clientConfig.auth = {
|
||||
username: config.username,
|
||||
password: config.password,
|
||||
};
|
||||
}
|
||||
|
||||
return new Client(clientConfig);
|
||||
}
|
||||
|
||||
export async function waitForOpenSearch(
|
||||
client: Client,
|
||||
maxRetries = 30,
|
||||
delayMs = 1000,
|
||||
): Promise<void> {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
await client.ping();
|
||||
console.log('OpenSearch is ready');
|
||||
return;
|
||||
} catch (error) {
|
||||
console.log(`Waiting for OpenSearch... (${i + 1}/${maxRetries})`);
|
||||
if (i === maxRetries - 1) {
|
||||
throw new Error(
|
||||
`OpenSearch is not available after ${maxRetries} retries`,
|
||||
);
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearOpenSearchIndices(client: Client): Promise<void> {
|
||||
try {
|
||||
const response = await client.cat.indices({ format: 'json' });
|
||||
const indices = response.body as Array<{ index: string }>;
|
||||
|
||||
const channelIndices = indices
|
||||
.filter((index) => index.index.startsWith('channel_'))
|
||||
.map((index) => index.index);
|
||||
|
||||
if (channelIndices.length > 0) {
|
||||
console.log(`Deleting OpenSearch indices: ${channelIndices.join(', ')}`);
|
||||
await client.indices.delete({ index: channelIndices });
|
||||
console.log('OpenSearch indices cleared');
|
||||
} else {
|
||||
console.log('No channel indices found in OpenSearch');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error clearing OpenSearch indices:', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function initializeOpenSearchForTest(): Promise<void> {
|
||||
const config: OpenSearchConfig = {
|
||||
node: 'http://localhost:9200',
|
||||
};
|
||||
|
||||
const client = await createOpenSearchClient(config);
|
||||
|
||||
try {
|
||||
await waitForOpenSearch(client);
|
||||
await clearOpenSearchIndices(client);
|
||||
console.log('OpenSearch initialized for test');
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize OpenSearch:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function cleanupOpenSearchAfterTest(): Promise<void> {
|
||||
const config: OpenSearchConfig = {
|
||||
node: 'http://localhost:9200',
|
||||
};
|
||||
|
||||
const client = await createOpenSearchClient(config);
|
||||
|
||||
try {
|
||||
await clearOpenSearchIndices(client);
|
||||
console.log('OpenSearch cleaned up after test');
|
||||
} catch (error) {
|
||||
console.error('Failed to cleanup OpenSearch:', error);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user