Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Playwright #1823

Merged
merged 38 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
24b1192
Add Playwright dependencies
xmatthias Nov 19, 2023
e5de9e5
Add initial playwright test
xmatthias Nov 19, 2023
a50a1d8
Add Playwright to scripts
xmatthias Nov 19, 2023
482793d
Add playwright to Ci setup
xmatthias Nov 19, 2023
49cc132
Remove pointless log statement
xmatthias Nov 19, 2023
122c668
Update package format and yarn.lock
xmatthias Apr 7, 2024
a2968d5
Fix playwright start command
xmatthias Apr 7, 2024
1a120cf
Fix test after rebase
xmatthias Apr 7, 2024
2937aac
Improve playwright helper
xmatthias Apr 7, 2024
6695310
Add initial trade test
xmatthias Apr 7, 2024
f2c584c
Add testing for Movement bug
xmatthias Apr 7, 2024
68f5442
Restore tests for modal testing
xmatthias Apr 7, 2024
36df4f9
Add id to MsgBoxModal
xmatthias Apr 7, 2024
0ae16e9
Enhance GH action runs
xmatthias Apr 8, 2024
3177cbc
Adapt vitest to exclude e2e tests
xmatthias Apr 8, 2024
03d77ec
Run playwright before cypress
xmatthias Apr 8, 2024
faf03f7
Improve playwright setup
xmatthias Apr 8, 2024
6463fea
Reorg trade test
xmatthias Apr 8, 2024
7229c4d
Skip tests on firefox for now
xmatthias Apr 8, 2024
97b625d
Split Test to reduce time
xmatthias Apr 8, 2024
e619f04
Reenable firefox ...
xmatthias Apr 8, 2024
2f455af
Use improved locator syntax
xmatthias Apr 8, 2024
0fca97b
Attempt to improve test resilience
xmatthias Apr 8, 2024
1258577
Upload playwright report to different files
xmatthias Apr 8, 2024
59578a4
Fix logic mistake in test assertion
xmatthias Apr 8, 2024
47de346
Add settings playwright test
xmatthias Apr 8, 2024
5a47510
Playwright backtesting tests
xmatthias Apr 8, 2024
a2fe720
Add pairlists playwright test
xmatthias Apr 8, 2024
a622ab6
Refactor mock system for playwright
xmatthias Apr 8, 2024
95df87c
Add chart spec
xmatthias Apr 9, 2024
75ed9bb
ad assert for chart page whitelist call
xmatthias Apr 9, 2024
c2f693c
Add dashboard playwright test
xmatthias Apr 9, 2024
09a3b56
Add initial few tests for login flow
xmatthias Apr 9, 2024
2be56db
Playwright: test login flow
xmatthias Apr 9, 2024
927b3f8
Complete login test
xmatthias Apr 9, 2024
c4e3993
migrate login failure test
xmatthias Apr 9, 2024
32219bd
Add final login test
xmatthias Apr 9, 2024
d6e4b58
Improve login test
xmatthias Apr 9, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,7 @@ yarn-error.log*


components.d.ts
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
51 changes: 51 additions & 0 deletions e2e/backtest.spec.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 40 additions & 0 deletions e2e/chart.spec.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 39 additions & 0 deletions e2e/dashboard.spec.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 84 additions & 0 deletions e2e/helpers.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 125 additions & 0 deletions e2e/login.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
Loading
Loading