Merge pull request #1823 from freqtrade/playwright

Playwright
This commit is contained in:
Matthias 2024-04-09 19:38:50 +02:00 committed by GitHub
commit 270004f643
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 832 additions and 3 deletions

View File

@ -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
View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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();
}
});
});

View File

@ -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
View 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,
},
});

View File

@ -11,6 +11,7 @@
@duplicate="plotStore.duplicatePlotConfig"
>
<b-form-select
id="plotConfigSelect"
v-model="plotStore.plotConfigName"
:options="plotStore.availablePlotConfigNames"
size="sm"

View File

@ -1,5 +1,6 @@
<template>
<b-modal
id="MsgBoxModal"
ref="removeTradeModal"
v-model="showRef"
:title="title"

View File

@ -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.*',
],
},
});

View File

@ -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"