mirror of
https://github.com/freqtrade/frequi.git
synced 2024-11-10 10:21:55 +00:00
commit
270004f643
16
.github/workflows/ci.yml
vendored
16
.github/workflows/ci.yml
vendored
|
@ -41,6 +41,22 @@ jobs:
|
|||
- name: Run Tests
|
||||
run: yarn test:unit
|
||||
|
||||
# Playwright section
|
||||
- name: Install Playwright Browsers
|
||||
run: yarn playwright install --with-deps
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: yarn playwright test
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-report-${{ matrix.node }}
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
# End Playwright section
|
||||
|
||||
- name: Run Component tests
|
||||
uses: cypress-io/github-action@v6
|
||||
with:
|
||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -34,3 +34,7 @@ yarn-error.log*
|
|||
|
||||
|
||||
components.d.ts
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
|
|
51
e2e/backtest.spec.ts
Normal file
51
e2e/backtest.spec.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { setLoginInfo, defaultMocks } from './helpers';
|
||||
|
||||
test.describe('Backtesting', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await defaultMocks(page);
|
||||
page.route('**/api/v1/show_config', (route) => {
|
||||
return route.fulfill({ path: `./cypress/fixtures/backtest/show_config_webserver.json` });
|
||||
});
|
||||
page.route('**/api/v1/strategies', (route) => {
|
||||
return route.fulfill({ path: `./cypress/fixtures/backtest/strategies.json` });
|
||||
});
|
||||
|
||||
await page.route('**/api/v1/backtest', (route) => {
|
||||
if (route.request().method() === 'POST') {
|
||||
route.fulfill({
|
||||
path: './cypress/fixtures/backtest/backtest_post_start.json',
|
||||
});
|
||||
} else if (route.request().method() === 'GET') {
|
||||
route.fulfill({
|
||||
path: './cypress/fixtures/backtest/backtest_get_end.json',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await setLoginInfo(page);
|
||||
});
|
||||
test('Starts webserver mode', async ({ page }) => {
|
||||
await page.goto('/backtest');
|
||||
|
||||
await expect(page.locator('a', { hasText: 'Backtest' })).toBeInViewport();
|
||||
await expect(page.getByText('Run backtest')).toBeInViewport();
|
||||
await expect(page.getByText('Strategy', { exact: true })).toBeInViewport();
|
||||
|
||||
const strategySelect = page.locator('select[id="strategy-select"]');
|
||||
await expect(strategySelect).toBeVisible();
|
||||
await expect(strategySelect).toBeInViewport();
|
||||
|
||||
await strategySelect.selectOption('SampleStrategy');
|
||||
const option = page.locator('option[value="SampleStrategy"]');
|
||||
await expect(option).toBeAttached();
|
||||
const analyzeButton = page.locator('[id="bt-analyze-btn"]');
|
||||
await expect(analyzeButton).toBeDisabled();
|
||||
|
||||
const startBacktestButton = page.locator('button[id="start-backtest"]');
|
||||
await Promise.all([startBacktestButton.click(), page.waitForResponse('**/api/v1/backtest')]);
|
||||
|
||||
// All buttons are now enabled
|
||||
await expect(analyzeButton).toBeEnabled();
|
||||
});
|
||||
});
|
40
e2e/chart.spec.ts
Normal file
40
e2e/chart.spec.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { setLoginInfo, defaultMocks } from './helpers';
|
||||
|
||||
test.describe('Chart', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await defaultMocks(page);
|
||||
await setLoginInfo(page);
|
||||
});
|
||||
test('Chart page', async ({ page }) => {
|
||||
await Promise.all([
|
||||
page.goto('/graph'),
|
||||
page.waitForResponse('**/whitelist'),
|
||||
page.waitForResponse('**/blacklist'),
|
||||
]);
|
||||
|
||||
// await page.waitForResponse('**/pair_candles');
|
||||
await page.locator('input[title="AutoRefresh"]').click();
|
||||
// await page.click('input[title="AutoRefresh"]');
|
||||
|
||||
await page.waitForSelector('span:has-text("NoActionStrategyFut | 1m")');
|
||||
|
||||
await page.click('.form-check:has-text("Heikin Ashi")');
|
||||
|
||||
// Reload triggers a new request
|
||||
await Promise.all([
|
||||
page.getByRole('button', { name: 'Refresh chart' }).click(),
|
||||
|
||||
page.waitForResponse('**/pair_candles?*'),
|
||||
]);
|
||||
// Disable Heikin Ashi
|
||||
await page.locator('.form-check:has-text("Heikin Ashi")').click();
|
||||
// Default plotconfig exists
|
||||
await expect(
|
||||
page
|
||||
.locator('div')
|
||||
.filter({ hasText: /^Heikin Ashidefault$/ })
|
||||
.locator('#plotConfigSelect'),
|
||||
).toHaveValue('default');
|
||||
});
|
||||
});
|
39
e2e/dashboard.spec.ts
Normal file
39
e2e/dashboard.spec.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { setLoginInfo, defaultMocks, tradeMocks } from './helpers';
|
||||
|
||||
test.describe('Dashboard', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await defaultMocks(page);
|
||||
await tradeMocks(page);
|
||||
await setLoginInfo(page);
|
||||
});
|
||||
test('Dashboard Page', async ({ page }) => {
|
||||
await Promise.all([
|
||||
page.goto('/dashboard'),
|
||||
page.waitForResponse('**/status'),
|
||||
page.waitForResponse('**/profit'),
|
||||
page.waitForResponse('**/balance'),
|
||||
// page.waitForResponse('**/trades'),
|
||||
page.waitForResponse('**/whitelist'),
|
||||
page.waitForResponse('**/blacklist'),
|
||||
page.waitForResponse('**/locks'),
|
||||
]);
|
||||
await expect(page.locator('.drag-header', { hasText: 'Bot comparison' })).toBeVisible();
|
||||
await expect(page.locator('.drag-header', { hasText: 'Bot comparison' })).toBeInViewport();
|
||||
await expect(page.locator('.drag-header', { hasText: 'Daily Profit' })).toBeVisible();
|
||||
await expect(page.locator('.drag-header', { hasText: 'Daily Profit' })).toBeInViewport();
|
||||
await expect(page.locator('.drag-header', { hasText: 'Open trades' })).toBeVisible();
|
||||
await expect(page.locator('.drag-header', { hasText: 'Open trades' })).toBeInViewport();
|
||||
await expect(page.locator('.drag-header', { hasText: 'Cumulative Profit' })).toBeVisible();
|
||||
await expect(page.locator('.drag-header', { hasText: 'Cumulative Profit' })).toBeInViewport();
|
||||
|
||||
await expect(page.locator('span', { hasText: 'TestBot' })).toBeVisible();
|
||||
await expect(page.locator('span', { hasText: 'Summary' })).toBeVisible();
|
||||
// Scroll to bottom
|
||||
await page.locator('.drag-header', { hasText: 'Trades Log' }).scrollIntoViewIfNeeded();
|
||||
await expect(page.locator('.drag-header', { hasText: 'Closed Trades' })).toBeInViewport();
|
||||
await expect(page.locator('.drag-header', { hasText: 'Profit Distribution' })).toBeInViewport();
|
||||
|
||||
await expect(page.locator('.drag-header', { hasText: 'Trades Log' })).toBeInViewport();
|
||||
});
|
||||
});
|
84
e2e/helpers.ts
Normal file
84
e2e/helpers.ts
Normal file
|
@ -0,0 +1,84 @@
|
|||
import { Page } from '@playwright/test';
|
||||
|
||||
export async function setLoginInfo(page) {
|
||||
await page.goto('/');
|
||||
await page.evaluate(() => {
|
||||
localStorage.setItem(
|
||||
'ftAuthLoginInfo',
|
||||
JSON.stringify({
|
||||
'ftbot.0': {
|
||||
botName: 'TestBot',
|
||||
apiUrl: 'http://localhost:3000',
|
||||
accessToken: 'access_token_tesst',
|
||||
refreshToken: 'refresh_test',
|
||||
autoRefresh: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
localStorage.setItem('ftSelectedBot', 'ftbot.0');
|
||||
});
|
||||
}
|
||||
|
||||
interface mockArray {
|
||||
name: string;
|
||||
url: string;
|
||||
fixture: string;
|
||||
method?: string;
|
||||
}
|
||||
|
||||
function mockRequests(page, mocks: mockArray[]) {
|
||||
mocks.forEach((item) => {
|
||||
page.route(item.url, (route) => {
|
||||
return route.fulfill({ path: `./cypress/fixtures/${item.fixture}` });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function defaultMocks(page: Page) {
|
||||
page.route('**/api/v1/**', (route) => {
|
||||
route.fulfill({
|
||||
headers: { 'access-control-allow-origin': '*' },
|
||||
json: {},
|
||||
});
|
||||
});
|
||||
|
||||
const mapping: mockArray[] = [
|
||||
{ name: '@Ping', url: '**/api/v1/ping', fixture: 'ping.json' },
|
||||
{ name: '@Ping', url: '**/api/v1/show_config', fixture: 'show_config.json' },
|
||||
{ name: '@Ping', url: '**/api/v1/pair_candles?*', fixture: 'pair_candles_btc_1m.json' },
|
||||
{ name: '@Whitelist', url: '**/api/v1/whitelist', fixture: 'whitelist.json' },
|
||||
{ name: '@Blacklist', url: '**/api/v1/blacklist', fixture: 'blacklist.json' },
|
||||
];
|
||||
|
||||
mockRequests(page, mapping);
|
||||
}
|
||||
|
||||
export function tradeMocks(page) {
|
||||
const mapping: mockArray[] = [
|
||||
{ name: '@Status', url: '**/api/v1/status', fixture: 'status_empty.json' },
|
||||
{ name: '@Profit', url: '**/api/v1/profit', fixture: 'profit.json' },
|
||||
{ name: '@Trades', url: '**/api/v1/trades*', fixture: 'trades.json' },
|
||||
{ name: '@Balance', url: '**/api/v1/balance', fixture: 'balance.json' },
|
||||
{ name: '@Locks', url: '**/api/v1/locks', fixture: 'locks_empty.json' },
|
||||
{ name: '@Performance', url: '**/api/v1/performance', fixture: 'performance.json' },
|
||||
{
|
||||
name: '@ReloadConfig',
|
||||
method: 'POST',
|
||||
url: '**/api/v1/reload_config',
|
||||
fixture: 'reload_config.json',
|
||||
},
|
||||
];
|
||||
mockRequests(page, mapping);
|
||||
}
|
||||
|
||||
export function getWaitForResponse(page: Page, url: string) {
|
||||
const urlMapping = {
|
||||
'@Ping': '**/api/v1/ping',
|
||||
'@ShowConf': '**/api/v1/show_config',
|
||||
'@PairCandles': '**/api/v1/pair_candles',
|
||||
'@Logs': '**/api/v1/logs',
|
||||
};
|
||||
const urlMap = urlMapping[url] ?? url;
|
||||
|
||||
return page.waitForResponse(urlMap);
|
||||
}
|
125
e2e/login.spec.ts
Normal file
125
e2e/login.spec.ts
Normal file
|
@ -0,0 +1,125 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { setLoginInfo, defaultMocks, tradeMocks } from './helpers';
|
||||
|
||||
test.describe('Login', () => {
|
||||
test('Is not logged in', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page.locator('button', { hasText: 'Login' })).toBeInViewport();
|
||||
|
||||
await page.locator('li', { hasText: 'No bot selected' });
|
||||
await page.locator('button:has-text("Login")').click();
|
||||
await page.locator('.modal-title:has-text("Login to your bot")');
|
||||
// Test prefilled URL
|
||||
await expect(page.locator('input[id=url-input]').inputValue()).resolves.toBe(
|
||||
'http://localhost:3000',
|
||||
);
|
||||
await page.locator('#name-input').isVisible();
|
||||
await page.locator('#username-input').isVisible();
|
||||
await page.locator('#password-input').isVisible();
|
||||
// Modal popup will use "OK" instead of "submit"
|
||||
await expect(page.locator('button[type=submit]')).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('Explicit login page', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
await expect(page.locator('button', { hasText: 'Login' })).not.toBeInViewport();
|
||||
await page.locator('li', { hasText: 'No bot selected' });
|
||||
await page.locator('.card-header:has-text("Freqtrade bot Login")');
|
||||
// Test prefilled URL
|
||||
await expect(page.locator('input[id=url-input]').inputValue()).resolves.toBe(
|
||||
'http://localhost:3000',
|
||||
);
|
||||
await page.locator('input[id=name-input]').isVisible();
|
||||
await page.locator('input[id=username-input]').isVisible();
|
||||
await page.locator('input[id=password-input]').isVisible();
|
||||
await page.locator('button[type=submit]').isVisible();
|
||||
});
|
||||
|
||||
test('Redirect when not logged in', async ({ page }) => {
|
||||
await page.goto('/trade');
|
||||
// await expect(page.locator('button', { hasText: 'Login' })).toBeInViewport();
|
||||
await expect(page.locator('li', { hasText: 'No bot selected' }).first()).toBeInViewport();
|
||||
await expect(page).toHaveURL(/.*\/login\?redirect=\/trade/);
|
||||
});
|
||||
test('Test Login', async ({ page }) => {
|
||||
await defaultMocks(page);
|
||||
await page.goto('/login');
|
||||
await page.locator('.card-header:has-text("Freqtrade bot Login")');
|
||||
await page.locator('input[id=name-input]').fill('TestBot');
|
||||
await page.locator('input[id=username-input]').fill('Freqtrader');
|
||||
await page.locator('input[id=password-input]').fill('SuperDuperBot');
|
||||
|
||||
await page.route('**/api/v1/token/login', (route) => {
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
json: { access_token: 'access_token_tesst', refresh_token: 'refresh_test' },
|
||||
headers: { 'access-control-allow-origin': '*' },
|
||||
});
|
||||
});
|
||||
const loginButton = await page.locator('button[type=submit]');
|
||||
await expect(loginButton).toBeVisible();
|
||||
await expect(loginButton).toContainText('Submit');
|
||||
await Promise.all([loginButton.click(), page.waitForResponse('**/api/v1/token/login')]);
|
||||
|
||||
await expect(page.locator('span', { hasText: 'TestBot' })).toBeVisible();
|
||||
await expect(page.locator('button', { hasText: 'Add new Bot' })).toBeVisible();
|
||||
await expect(page.locator('button', { hasText: 'Login' })).not.toBeVisible();
|
||||
// Test logout
|
||||
await page.locator('#avatar-drop').click();
|
||||
await page.locator('a:visible', { hasText: 'Sign Out' }).click();
|
||||
// Assert we're logged out again
|
||||
await expect(page.locator('button', { hasText: 'Login' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('Test Login failed - wrong api url', async ({ page }) => {
|
||||
await defaultMocks(page);
|
||||
await page.goto('/login');
|
||||
await page.locator('.card-header:has-text("Freqtrade bot Login")');
|
||||
await page.locator('input[id=name-input]').fill('TestBot');
|
||||
await page.locator('input[id=username-input]').fill('Freqtrader');
|
||||
await page.locator('input[id=password-input]').fill('SuperDuperBot');
|
||||
|
||||
await page.route('**/api/v1/token/login', (route) => {
|
||||
return route.fulfill({
|
||||
status: 404,
|
||||
json: { access_token: 'access_token_tesst', refresh_token: 'refresh_test' },
|
||||
headers: { 'access-control-allow-origin': '*' },
|
||||
});
|
||||
});
|
||||
const loginButton = await page.locator('button[type=submit]');
|
||||
await expect(loginButton).toBeVisible();
|
||||
await expect(loginButton).toContainText('Submit');
|
||||
await Promise.all([loginButton.click(), page.waitForResponse('**/api/v1/token/login')]);
|
||||
await expect(page.getByText('Login failed.')).toBeVisible();
|
||||
await expect(page.getByText('API Url required')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Test Login failed - wrong password', async ({ page }) => {
|
||||
await defaultMocks(page);
|
||||
await page.goto('/login');
|
||||
await page.locator('.card-header:has-text("Freqtrade bot Login")');
|
||||
await page.locator('input[id=name-input]').fill('TestBot');
|
||||
await page.locator('input[id=username-input]').fill('Freqtrader');
|
||||
await page.locator('input[id=password-input]').fill('SuperDuperBot');
|
||||
|
||||
await page.route('**/api/v1/token/login', (route) => {
|
||||
return route.fulfill({
|
||||
status: 401,
|
||||
json: { access_token: 'access_token_tesst', refresh_token: 'refresh_test' },
|
||||
headers: { 'access-control-allow-origin': '*' },
|
||||
});
|
||||
});
|
||||
|
||||
const loginButton = await page.locator('button[type=submit]');
|
||||
await expect(loginButton).toBeVisible();
|
||||
await expect(loginButton).toContainText('Submit');
|
||||
await expect(page.getByText('Name and Password are required.')).not.toBeVisible();
|
||||
await expect(page.getByText('Connected to bot, however Login failed,')).not.toBeVisible();
|
||||
await expect(page.getByText('Invalid Password')).not.toBeVisible();
|
||||
|
||||
await Promise.all([loginButton.click(), page.waitForResponse('**/api/v1/token/login')]);
|
||||
await expect(page.getByText('Name and Password are required.')).toBeVisible();
|
||||
await expect(page.getByText('Invalid Password')).toBeVisible();
|
||||
await expect(page.getByText('Connected to bot, however Login failed,')).toBeVisible();
|
||||
});
|
||||
});
|
32
e2e/logs.spec.ts
Normal file
32
e2e/logs.spec.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
|
||||
import { setLoginInfo, defaultMocks, getWaitForResponse } from './helpers';
|
||||
|
||||
test.describe('Logs', () => {
|
||||
test('Displays and reloads logs', async ({ page }) => {
|
||||
///
|
||||
await defaultMocks(page);
|
||||
await setLoginInfo(page);
|
||||
// const pingPromise = page.route('**/*ping*',
|
||||
|
||||
// const logsPromise = page.waitForResponse('**/api/v1/logs');
|
||||
await page.route('**/api/v1/logs', (route) => {
|
||||
return route.fulfill({ path: './cypress/fixtures/logs.json' });
|
||||
});
|
||||
|
||||
const logs = getWaitForResponse(page, '@Logs');
|
||||
const ping = getWaitForResponse(page, '@ShowConf');
|
||||
await page.goto('/logs', { waitUntil: 'networkidle' });
|
||||
await Promise.all([logs, ping]);
|
||||
|
||||
await expect(page.locator('span', { hasText: 'Checking exchange' })).toBeVisible();
|
||||
await expect(page.locator('span', { hasText: 'Checking exchange' })).toHaveText(
|
||||
/Checking exchange.../,
|
||||
{},
|
||||
);
|
||||
// const logsPromise = page.waitForResponse('**/api/v1/logs');
|
||||
const logsPromise = getWaitForResponse(page, '@Logs');
|
||||
await page.getByRole('button', { name: 'Reload Logs' }).click();
|
||||
await logsPromise;
|
||||
});
|
||||
});
|
98
e2e/pairlists.spec.ts
Normal file
98
e2e/pairlists.spec.ts
Normal file
|
@ -0,0 +1,98 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
|
||||
import { setLoginInfo, defaultMocks } from './helpers';
|
||||
|
||||
test.describe('Pairlists', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await defaultMocks(page);
|
||||
page.route('**/api/v1/show_config', (route) => {
|
||||
return route.fulfill({ path: `./cypress/fixtures/backtest/show_config_webserver.json` });
|
||||
});
|
||||
|
||||
page.route('**/api/v1/pairlists/available*', (route) => {
|
||||
return route.fulfill({ path: `./cypress/fixtures/pairlists_available.json` });
|
||||
});
|
||||
|
||||
const jobID = 'c0578b6a-dd34-4bb7-b83c-492f02da2cfd';
|
||||
|
||||
page.route('**/api/v1/pairlists/evaluate', (route) => {
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
status: 'Pairlist evaluation started in background.',
|
||||
job_id: jobID,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
page.route(`**/api/v1/background/${jobID}`, (route) => {
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
job_id: jobID,
|
||||
job_category: 'pairlist',
|
||||
status: 'success',
|
||||
running: false,
|
||||
progress: null,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
page.route(`**/api/v1/pairlists/evaluate/${jobID}`, (route) => {
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
error: null,
|
||||
status: 'success',
|
||||
result: {
|
||||
whitelist: ['BTC/USDT', 'ETH/USDT', 'BNB/USDT'],
|
||||
length: 3,
|
||||
method: ['VolumePairList'],
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
await setLoginInfo(page);
|
||||
});
|
||||
test('Pairlists page', async ({ page }) => {
|
||||
page.goto('/');
|
||||
|
||||
const pairlistConfig = page.locator('a', { hasText: 'Pairlist Config' });
|
||||
|
||||
await Promise.all([pairlistConfig.click(), page.waitForResponse('**/pairlists/available*')]);
|
||||
|
||||
const volumePairList = page.locator('text="VolumePairList"');
|
||||
await expect(volumePairList).toBeVisible();
|
||||
|
||||
const volumePairListButton = page.locator('.available-pairlists :nth-child(4) > .btn');
|
||||
await expect(volumePairListButton).toBeInViewport();
|
||||
await volumePairListButton.click();
|
||||
|
||||
const resultsButton = page.locator('.btn', { hasText: 'Results' });
|
||||
// await expect(resultsButton).toHaveAttribute('value', 'Results');
|
||||
await expect(resultsButton).toBeDisabled();
|
||||
|
||||
const copyContainer = page.locator('.copy-container');
|
||||
await expect(copyContainer).toBeVisible();
|
||||
await expect(copyContainer).toContainText('"method": "VolumePairList",');
|
||||
|
||||
const evaluateButton = page.locator('button >> text="Evaluate"');
|
||||
|
||||
await Promise.all([
|
||||
evaluateButton.click(),
|
||||
page.waitForResponse('**/evaluate'),
|
||||
page.waitForResponse('**/background/*'),
|
||||
page.waitForResponse('**/pairlists/evaluate/*'),
|
||||
]);
|
||||
|
||||
await expect(resultsButton).toBeEnabled();
|
||||
await expect(resultsButton).toBeChecked();
|
||||
|
||||
await expect(copyContainer).toContainText('"BTC/USDT",');
|
||||
await expect(copyContainer).toContainText('"ETH/USDT",');
|
||||
await expect(copyContainer).toContainText('"BNB/USDT"');
|
||||
});
|
||||
});
|
33
e2e/settings.spec.ts
Normal file
33
e2e/settings.spec.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { setLoginInfo, defaultMocks } from './helpers';
|
||||
|
||||
test.describe('Settings', () => {
|
||||
test('Settings stores', async ({ page }) => {
|
||||
await setLoginInfo(page);
|
||||
await defaultMocks(page);
|
||||
await Promise.all([
|
||||
page.goto('/'),
|
||||
// page.waitForResponse('**/Ping'),
|
||||
// page.waitForResponse('**/ShowConf'),
|
||||
]);
|
||||
// await expect(page.locator('li', { hasText: 'Online' })).toBeInViewport();
|
||||
await expect(page.locator('h1', { hasText: 'Welcome to the Freqtrade UI' })).toBeInViewport({
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
await page
|
||||
.locator('[id=avatar-drop]')
|
||||
.isVisible()
|
||||
.then(() => page.locator('[id=avatar-drop]').click());
|
||||
await page.locator('.dropdown-menu > * > [href="/settings"]').click();
|
||||
await expect(page.locator(':text("FreqUI Settings")')).toBeVisible();
|
||||
|
||||
// Switch option in the settings.
|
||||
await page.locator('select').first().selectOption('asTitle');
|
||||
|
||||
const settings = await page.evaluate(() =>
|
||||
JSON.parse(window.localStorage.getItem('ftUISettings') || '{}'),
|
||||
);
|
||||
await expect(settings['openTradesInTitle']).toBe('asTitle');
|
||||
});
|
||||
});
|
127
e2e/trade.spec.ts
Normal file
127
e2e/trade.spec.ts
Normal file
|
@ -0,0 +1,127 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
|
||||
import { setLoginInfo, defaultMocks, tradeMocks } from './helpers';
|
||||
|
||||
test.describe('Trade', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await defaultMocks(page);
|
||||
await setLoginInfo(page);
|
||||
|
||||
await tradeMocks(page);
|
||||
});
|
||||
test('Trade page', async ({ page }) => {
|
||||
await Promise.all([
|
||||
page.goto('/trade'),
|
||||
// Wait for network requests
|
||||
// page.waitForResponse('**/ping'),
|
||||
page.waitForResponse('**/status'),
|
||||
page.waitForResponse('**/profit'),
|
||||
page.waitForResponse('**/balance'),
|
||||
// page.waitForResponse('**/trades'),
|
||||
page.waitForResponse('**/whitelist'),
|
||||
page.waitForResponse('**/blacklist'),
|
||||
page.waitForResponse('**/locks'),
|
||||
]);
|
||||
|
||||
// // Check visibility of elements
|
||||
await expect(page.locator('.drag-header', { hasText: 'Multi Pane' })).toBeInViewport();
|
||||
await expect(page.locator('.drag-header', { hasText: 'Chart' })).toBeInViewport();
|
||||
// Pairlist elements
|
||||
await expect(page.locator('button', { hasText: 'BTC/USDT' })).toBeInViewport();
|
||||
await expect(page.locator('button', { hasText: 'ETH/USDT' })).toBeInViewport();
|
||||
|
||||
// // Click on Performance button and wait for response
|
||||
await Promise.all([
|
||||
page.waitForResponse('**/performance'),
|
||||
page.click('button:has-text("Performance")'),
|
||||
]);
|
||||
|
||||
// // Check visibility of Profit USDT
|
||||
await expect(page.locator('th:has-text("Profit USDT")')).toBeInViewport();
|
||||
|
||||
// // Test messageBox behavior
|
||||
|
||||
const dialogModal = page.getByRole('dialog');
|
||||
const modalButton = page.locator(
|
||||
'#MsgBoxModal .modal-dialog > .modal-content > .modal-footer > .btn-secondary:has-text("Cancel")',
|
||||
);
|
||||
await expect(dialogModal).not.toBeVisible();
|
||||
await expect(dialogModal).not.toBeInViewport();
|
||||
|
||||
await expect(modalButton).not.toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Stop Trading - Also stops' }).click();
|
||||
|
||||
// Modal open
|
||||
await expect(dialogModal).toBeVisible();
|
||||
await expect(dialogModal).toBeInViewport();
|
||||
await expect(modalButton).toBeInViewport();
|
||||
|
||||
// // Close modal
|
||||
await modalButton.click();
|
||||
|
||||
// // Modal closed
|
||||
await expect(modalButton).not.toBeVisible();
|
||||
await expect(modalButton).not.toBeInViewport();
|
||||
|
||||
// // Click on General tab
|
||||
const performancePair = page.locator('td:has-text("XRP/USDT")');
|
||||
await expect(performancePair).toBeInViewport();
|
||||
await page.click('button[role="tab"]:has-text("General")');
|
||||
|
||||
// // Check visibility of elements
|
||||
await expect(performancePair).not.toBeInViewport();
|
||||
const openTrades = page.locator('.drag-header:has-text("Open Trades")');
|
||||
openTrades.scrollIntoViewIfNeeded();
|
||||
await expect(openTrades).toBeInViewport();
|
||||
const closedTrades = page.locator('.drag-header:has-text("Closed Trades")');
|
||||
closedTrades.scrollIntoViewIfNeeded();
|
||||
await expect(closedTrades).toBeInViewport();
|
||||
await expect(page.locator('span:has-text("TRX/USDT")')).toBeInViewport();
|
||||
await expect(page.locator('td:has-text("8070.5")')).toBeInViewport();
|
||||
|
||||
// Scroll to top
|
||||
const multiPane = page.locator('.drag-header', { hasText: 'Multi Pane' });
|
||||
await expect(multiPane).toBeVisible();
|
||||
await multiPane.scrollIntoViewIfNeeded();
|
||||
await expect(multiPane).toBeInViewport();
|
||||
|
||||
// // Click on Reload Config button
|
||||
await page.getByRole('button', { name: 'Reload Config' }).click();
|
||||
// await page.locator('button[title*="Reload Config "]').click();
|
||||
await expect(dialogModal).toBeVisible();
|
||||
await expect(dialogModal).toBeInViewport();
|
||||
|
||||
const modalOkButton = page.locator(
|
||||
'#MsgBoxModal .modal-dialog > .modal-content > .modal-footer > .btn-primary:has-text("Ok")',
|
||||
);
|
||||
await expect(modalOkButton).toBeVisible();
|
||||
await modalOkButton.click();
|
||||
|
||||
await expect(page.getByText('Config reloaded successfully.')).toBeInViewport();
|
||||
});
|
||||
test('Trade page - drag and drop', async ({ page }) => {
|
||||
await page.goto('/trade');
|
||||
|
||||
await page.locator('#avatar-drop').click();
|
||||
const multiPane = page.locator('.drag-header', { hasText: 'Multi Pane' });
|
||||
|
||||
await page.getByLabel('Lock layout').uncheck();
|
||||
|
||||
const chartHeader = await page.locator('.drag-header:has-text("Chart")');
|
||||
await expect(multiPane).toBeInViewport();
|
||||
await expect(chartHeader).toBeInViewport();
|
||||
|
||||
// Test drag and drop functionality
|
||||
const chartHeaderbb = await chartHeader.boundingBox();
|
||||
if (chartHeaderbb) {
|
||||
await chartHeader.hover();
|
||||
await page.mouse.down();
|
||||
|
||||
await page.mouse.move(chartHeaderbb?.x + chartHeaderbb.width / 2, chartHeaderbb?.y + 200);
|
||||
await page.mouse.up();
|
||||
await expect(multiPane).toBeInViewport();
|
||||
await expect(chartHeader).toBeInViewport();
|
||||
}
|
||||
});
|
||||
});
|
|
@ -14,7 +14,10 @@
|
|||
"cy:open": "cypress open",
|
||||
"cy:run": "cypress run",
|
||||
"cy:open-ct": "cypress open-ct",
|
||||
"cy:run-ct": "cypress run --component"
|
||||
"cy:run-ct": "cypress run --component",
|
||||
"test:e2e": "yarn playwright test",
|
||||
"test:e2e-chromium": "yarn playwright test --project=chromium",
|
||||
"test:e2e-msedge": "yarn playwright test --project=msedge"
|
||||
},
|
||||
"dependencies": {
|
||||
"@noction/vue-draggable-grid": "1.9.16",
|
||||
|
@ -45,7 +48,9 @@
|
|||
"@cypress/vite-dev-server": "^5.0.7",
|
||||
"@cypress/vue": "^6.0.0",
|
||||
"@iconify-json/mdi": "^1.1.64",
|
||||
"@playwright/test": "^1.40.0",
|
||||
"@types/echarts": "^4.9.22",
|
||||
"@types/node": "^20.9.2",
|
||||
"@typescript-eslint/eslint-plugin": "^7.5.0",
|
||||
"@typescript-eslint/parser": "^7.5.0",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
|
|
80
playwright.config.ts
Normal file
80
playwright.config.ts
Normal file
|
@ -0,0 +1,80 @@
|
|||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
/* 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: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: '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://localhost: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'], channel: 'chrome' },
|
||||
},
|
||||
{
|
||||
name: 'msedge',
|
||||
use: { ...devices['Desktop Edge'], channel: 'msedge' }, // or "msedge-beta" or 'msedge-dev'
|
||||
},
|
||||
{
|
||||
name: 'firefox',
|
||||
use: { ...devices['Desktop Firefox'] },
|
||||
},
|
||||
|
||||
{
|
||||
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:3000',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
});
|
|
@ -11,6 +11,7 @@
|
|||
@duplicate="plotStore.duplicatePlotConfig"
|
||||
>
|
||||
<b-form-select
|
||||
id="plotConfigSelect"
|
||||
v-model="plotStore.plotConfigName"
|
||||
:options="plotStore.availablePlotConfigNames"
|
||||
size="sm"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<b-modal
|
||||
id="MsgBoxModal"
|
||||
ref="removeTradeModal"
|
||||
v-model="showRef"
|
||||
:title="title"
|
||||
|
|
|
@ -63,4 +63,14 @@ export default defineConfig({
|
|||
host: '127.0.0.1',
|
||||
port: 3000,
|
||||
},
|
||||
test: {
|
||||
exclude: [
|
||||
'**/node_modules/**',
|
||||
'**/dist/**',
|
||||
'**/cypress/**',
|
||||
'**/e2e/**',
|
||||
'**/.{idea,git,cache,output,temp}/**',
|
||||
'**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',
|
||||
],
|
||||
},
|
||||
});
|
||||
|
|
87
yarn.lock
87
yarn.lock
|
@ -765,6 +765,17 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@playwright/test@npm:^1.40.0":
|
||||
version: 1.40.0
|
||||
resolution: "@playwright/test@npm:1.40.0"
|
||||
dependencies:
|
||||
playwright: "npm:1.40.0"
|
||||
bin:
|
||||
playwright: cli.js
|
||||
checksum: 10/46ebd396d37b3e438019229f5c84bf4a8614f455245a51fd2d77a05f6065aefc41af30f26882415607c9e4adc2bb1511bd1f1ba06d7870fae0b5525768240acb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@popperjs/core@npm:^2.11.8":
|
||||
version: 2.11.8
|
||||
resolution: "@popperjs/core@npm:2.11.8"
|
||||
|
@ -939,6 +950,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:^20.9.2":
|
||||
version: 20.9.2
|
||||
resolution: "@types/node@npm:20.9.2"
|
||||
dependencies:
|
||||
undici-types: "npm:~5.26.4"
|
||||
checksum: 10/8bab2870bfc02efc988c53dfb0149634f8feb824132cc7f20b36f3d55d89ef893e3a43d545524a5cb3a284f4ce68ae4181d75a4a39cee6b79c586d719e6b7461
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/semver@npm:^7.5.0":
|
||||
version: 7.5.8
|
||||
resolution: "@types/semver@npm:7.5.8"
|
||||
|
@ -1483,7 +1503,16 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"acorn@npm:^8.10.0, acorn@npm:^8.11.2, acorn@npm:^8.11.3, acorn@npm:^8.9.0":
|
||||
"acorn@npm:^8.10.0, acorn@npm:^8.9.0":
|
||||
version: 8.11.2
|
||||
resolution: "acorn@npm:8.11.2"
|
||||
bin:
|
||||
acorn: bin/acorn
|
||||
checksum: 10/ff559b891382ad4cd34cc3c493511d0a7075a51f5f9f02a03440e92be3705679367238338566c5fbd3521ecadd565d29301bc8e16cb48379206bffbff3d72500
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"acorn@npm:^8.11.2, acorn@npm:^8.11.3":
|
||||
version: 8.11.3
|
||||
resolution: "acorn@npm:8.11.3"
|
||||
bin:
|
||||
|
@ -3047,8 +3076,10 @@ __metadata:
|
|||
"@cypress/vue": "npm:^6.0.0"
|
||||
"@iconify-json/mdi": "npm:^1.1.64"
|
||||
"@noction/vue-draggable-grid": "npm:1.9.16"
|
||||
"@playwright/test": "npm:^1.40.0"
|
||||
"@popperjs/core": "npm:^2.11.8"
|
||||
"@types/echarts": "npm:^4.9.22"
|
||||
"@types/node": "npm:^20.9.2"
|
||||
"@typescript-eslint/eslint-plugin": "npm:^7.5.0"
|
||||
"@typescript-eslint/parser": "npm:^7.5.0"
|
||||
"@vitejs/plugin-vue": "npm:^5.0.4"
|
||||
|
@ -3133,6 +3164,16 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fsevents@npm:2.3.2":
|
||||
version: 2.3.2
|
||||
resolution: "fsevents@npm:2.3.2"
|
||||
dependencies:
|
||||
node-gyp: "npm:latest"
|
||||
checksum: 10/6b5b6f5692372446ff81cf9501c76e3e0459a4852b3b5f1fc72c103198c125a6b8c72f5f166bdd76ffb2fca261e7f6ee5565daf80dca6e571e55bcc589cc1256
|
||||
conditions: os=darwin
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fsevents@npm:~2.3.2, fsevents@npm:~2.3.3":
|
||||
version: 2.3.3
|
||||
resolution: "fsevents@npm:2.3.3"
|
||||
|
@ -3143,6 +3184,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin<compat/fsevents>":
|
||||
version: 2.3.2
|
||||
resolution: "fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin<compat/fsevents>::version=2.3.2&hash=df0bf1"
|
||||
dependencies:
|
||||
node-gyp: "npm:latest"
|
||||
conditions: os=darwin
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin<compat/fsevents>, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin<compat/fsevents>":
|
||||
version: 2.3.3
|
||||
resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin<compat/fsevents>::version=2.3.3&hash=df0bf1"
|
||||
|
@ -3940,7 +3990,16 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0":
|
||||
"lru-cache@npm:^10.0.1":
|
||||
version: 10.0.2
|
||||
resolution: "lru-cache@npm:10.0.2"
|
||||
dependencies:
|
||||
semver: "npm:^7.3.5"
|
||||
checksum: 10/a675b71a19f4b23186549e343792c3eb6196a5fca2a96b59e31a44289459b7e166b3c6cb08952f45ac29d8cfe561cabee88d906fdd5c98fb7cbda8f5d47431a3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lru-cache@npm:^10.2.0":
|
||||
version: 10.2.0
|
||||
resolution: "lru-cache@npm:10.2.0"
|
||||
checksum: 10/502ec42c3309c0eae1ce41afca471f831c278566d45a5273a0c51102dee31e0e250a62fa9029c3370988df33a14188a38e682c16143b794de78668de3643e302
|
||||
|
@ -4591,6 +4650,30 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"playwright-core@npm:1.40.0":
|
||||
version: 1.40.0
|
||||
resolution: "playwright-core@npm:1.40.0"
|
||||
bin:
|
||||
playwright-core: cli.js
|
||||
checksum: 10/2ce5245988b0e89ed3359476a83ad724484da349eae42e6c9dd4162a2180b1d37fe1750df45473a5a04a1590cb093d9f0df81e607b7f5c2c161f881436ee1a00
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"playwright@npm:1.40.0":
|
||||
version: 1.40.0
|
||||
resolution: "playwright@npm:1.40.0"
|
||||
dependencies:
|
||||
fsevents: "npm:2.3.2"
|
||||
playwright-core: "npm:1.40.0"
|
||||
dependenciesMeta:
|
||||
fsevents:
|
||||
optional: true
|
||||
bin:
|
||||
playwright: cli.js
|
||||
checksum: 10/56352a177e712598f30bbe6da69027a065cb9ee50df512e186dfc9e479a68bd60bab2fee9278e943ab75d4109ad062b88556f3e7efdabfd1f91f728ccceee631
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"portal-vue@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "portal-vue@npm:3.0.0"
|
||||
|
|
Loading…
Reference in New Issue
Block a user