Pull request #2367: ADG-9679 add playwright test from python
Some checks failed
build / test (macOS-latest) (push) Has been cancelled
build / test (ubuntu-latest) (push) Has been cancelled
build / test (windows-latest) (push) Has been cancelled
build / build-release (push) Has been cancelled
build / notify (push) Has been cancelled
lint / go-lint (push) Has been cancelled
lint / eslint (push) Has been cancelled
lint / notify (push) Has been cancelled

Merge in DNS/adguard-home from ADG-9679 to master

Squashed commit of the following:

commit d2a759b4636b7ec931bfba625827c8b91c60c7e7
Merge: a5e7eea16 9726171f0
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Mon Apr 14 16:05:01 2025 +0200

    Merge remote-tracking branch 'origin/master' into ADG-9679

commit a5e7eea16e6c29d25290ee79b1918df8af59bb51
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Mon Apr 14 13:56:26 2025 +0200

    vitest version bump

commit 26620d1923d92b3a6eb9b80a364748f2f6f66030
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Thu Apr 10 15:39:11 2025 +0200

    formatting

commit dbab03d1316241eaff0fc9c99d58a1933e415d2b
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Thu Apr 10 15:37:55 2025 +0200

    rollback experiments

commit 4427d984177786f7d905915cf8080166b45d7b46
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Thu Apr 10 15:33:28 2025 +0200

    checking dir structure

commit 2cf7eed247d2869ed285dbee0bf32cf1d8df7e86
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Thu Apr 10 15:21:11 2025 +0200

    fixed docker image builder

commit 8bd06f412fad9dd09df0e076879bd2cbd2f30d1a
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Thu Apr 10 15:18:33 2025 +0200

    home-js-builder version bump

commit 2a83bfeb322a20ec4278e359b18d0466966ec043
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Thu Apr 10 15:17:38 2025 +0200

    try to remove installation dependencies for e2e test (build is already available)

commit 163e4581e83152f99058b798484468009e8e88b0
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Thu Apr 10 13:52:27 2025 +0200

    Revert "changed nslookup to dig in e2e tests"
    
    This reverts commit ecb68200ea28e295f504338cc59c711b5540022b.

commit 15f7c5e2c77e230da77a0f9de0bd9cce8451da95
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Thu Apr 10 13:45:40 2025 +0200

    js-home-builder version bump

commit ecb68200ea28e295f504338cc59c711b5540022b
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Apr 9 15:07:39 2025 +0200

    changed nslookup to dig in e2e tests

commit 77c94a60be8839f3e0ad9d02e7dbb2ebd802d3d6
Author: Eugene Miroshkin <e.miroshkin@adguard.com>
Date:   Wed Apr 9 11:09:15 2025 +0300

    revert timeouts

commit 9dfebc8bcaf2cd3258b39fcbd0f67ab51c2eb46d
Merge: 912f4cb7b 1cc6c00e4
Author: Eugene Miroshkin <e.miroshkin@adguard.com>
Date:   Wed Apr 9 11:02:19 2025 +0300

    merge master

commit 912f4cb7b71f02866244fe447ca0e7fbd2a015bb
Author: Eugene Miroshkin <e.miroshkin@adguard.com>
Date:   Wed Apr 9 10:48:59 2025 +0300

    cleanup code

commit 9da200ebca5b001f4952f33d819d90c1938920ee
Author: Eugene Miroshkin <e.miroshkin@adguard.com>
Date:   Thu Apr 3 17:39:20 2025 +0300

    update tests

commit 794e0bd0a92a41c4d3827b716eeab584a25bd3ed
Author: Eugene Miroshkin <e.miroshkin@adguard.com>
Date:   Thu Mar 13 18:15:58 2025 +0300

    cleanup

commit 9a523b4e255dd24c0f640bc279924ed2c13509a9
Author: Eugene Miroshkin <e.miroshkin@adguard.com>
Date:   Thu Mar 13 18:04:34 2025 +0300

    ADG-9679 add playwright test from python
This commit is contained in:
Eugene Miroshkin
2025-04-14 18:03:45 +03:00
committed by Igor 🐧 Lobanov
parent 9726171f0f
commit 4d258972d1
15 changed files with 1054 additions and 565 deletions

View File

@@ -7,7 +7,7 @@
# Make sure to sync any changes with the branch overrides below.
'variables':
'channel': 'edge'
'dockerFrontend': 'adguard/home-js-builder:3.0'
'dockerFrontend': 'adguard/home-js-builder:3.1'
'dockerGo': 'adguard/go-builder:1.24.2--1'
'stages':
@@ -278,7 +278,7 @@
# need to build a few of these.
'variables':
'channel': 'beta'
'dockerFrontend': 'adguard/home-js-builder:3.0'
'dockerFrontend': 'adguard/home-js-builder:3.1'
'dockerGo': 'adguard/go-builder:1.24.2--1'
# release-vX.Y.Z branches are the branches from which the actual final
# release is built.
@@ -294,5 +294,5 @@
# are the ones that actually get released.
'variables':
'channel': 'release'
'dockerFrontend': 'adguard/home-js-builder:3.0'
'dockerFrontend': 'adguard/home-js-builder:3.1'
'dockerGo': 'adguard/go-builder:1.24.2--1'

View File

@@ -5,7 +5,7 @@
'key': 'AHBRTSPECS'
'name': 'AdGuard Home - Build and run tests'
'variables':
'dockerFrontend': 'adguard/home-js-builder:3.0'
'dockerFrontend': 'adguard/home-js-builder:3.1'
'dockerGo': 'adguard/go-builder:1.24.2--1'
'channel': 'development'
@@ -233,6 +233,6 @@
# Set the default release channel on the release branch to beta, as we
# may need to build a few of these.
'variables':
'dockerFrontend': 'adguard/home-js-builder:3.0'
'dockerFrontend': 'adguard/home-js-builder:3.1'
'dockerGo': 'adguard/go-builder:1.24.2--1'
'channel': 'candidate'

1207
client/package-lock.json generated vendored

File diff suppressed because it is too large Load Diff

4
client/package.json vendored
View File

@@ -66,7 +66,7 @@
"@babel/preset-react": "^7.24.1",
"@playwright/test": "1.50.1",
"@types/lodash": "^4.17.4",
"@types/node": "^22.10.2",
"@types/node": "^22.13.10",
"@types/react": "^17.0.80",
"@types/react-dom": "^18.3.0",
"@types/react-redux": "^7.1.33",
@@ -99,7 +99,7 @@
"stylelint": "^16.5.0",
"ts-loader": "^9.5.1",
"url-loader": "^4.1.1",
"vitest": "^3.0.4",
"vitest": "^3.1.1",
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.4",

View File

@@ -78,6 +78,7 @@ class CustomRules extends Component<CustomRulesProps> {
<form onSubmit={this.handleSubmit}>
<div className="text-edit-container mb-4">
<textarea
data-testid="custom_rule_textarea"
className="form-control font-monospace text-input"
value={userRules}
onChange={this.handleChange}
@@ -91,6 +92,7 @@ class CustomRules extends Component<CustomRulesProps> {
<div className="card-actions">
<button
data-testid="apply_custom_rule"
className="btn btn-success btn-standard btn-large"
type="submit"
onClick={this.handleSubmit}>

View File

@@ -59,7 +59,7 @@ const Header = () => {
<div className="header__column">
<div className="header__right">
{!processingProfile && name && (
<a href="control/logout" className="btn btn-sm btn-outline-secondary">
<a href="control/logout" className="btn btn-sm btn-outline-secondary" data-testid="sign_out">
{t('sign_out')}
</a>
)}

View File

@@ -288,7 +288,7 @@ const Row = memo(
);
return (
<div style={style} className={className} onClick={onClick} role="row">
<div style={style} className={className} onClick={onClick} role="row" data-testid="querylog_cell">
<DateCell {...rowProps} />
<DomainCell {...rowProps} />

View File

@@ -84,6 +84,7 @@ export const Form = ({ className, setIsLoading }: Props) => {
}}>
<div className="field__search">
<SearchField
data-testid="querylog_search"
value={searchValue}
handleChange={(val) => setValue('search', val)}
onKeyDown={onEnterPress}

View File

@@ -27,12 +27,14 @@ const SETTINGS = {
enabled: false,
title: i18next.t('use_adguard_browsing_sec'),
subtitle: i18next.t('use_adguard_browsing_sec_hint'),
testId: 'safebrowsing',
[ORDER_KEY]: 0,
},
parental: {
enabled: false,
title: i18next.t('use_adguard_parental'),
subtitle: i18next.t('use_adguard_parental_hint'),
testId: 'parental',
[ORDER_KEY]: 1,
},
};
@@ -90,11 +92,12 @@ class Settings extends Component<SettingsProps> {
renderSettings = (settings: any) =>
getObjectKeysSorted(SETTINGS, ORDER_KEY).map((key: any) => {
const setting = settings[key];
const { enabled, title, subtitle } = setting;
const { enabled, title, subtitle, testId } = setting;
return (
<div key={key} className="form__group form__group--checkbox">
<Checkbox
data-testid={testId}
value={enabled}
title={title}
subtitle={subtitle}
@@ -118,6 +121,7 @@ class Settings extends Component<SettingsProps> {
<>
<div className="form__group form__group--checkbox">
<Checkbox
data-testid="safesearch"
value={enabled}
title={i18next.t('enforce_safe_search')}
subtitle={i18next.t('enforce_save_search_hint')}

View File

@@ -94,14 +94,17 @@ const Footer = () => {
auto: {
desc: t('theme_auto_desc'),
icon: '#auto',
testId: 'theme_auto',
},
dark: {
desc: t('theme_dark_desc'),
icon: '#dark',
testId: 'theme_dark',
},
light: {
desc: t('theme_light_desc'),
icon: '#light',
testId: 'theme_light',
},
};
@@ -113,7 +116,9 @@ const Footer = () => {
type="button"
className="btn btn-sm btn-secondary footer__theme-button"
onClick={() => onThemeChange(theme)}
title={content[theme].desc}>
title={content[theme].desc}
data-testid={content[theme].testId}
>
<svg className={cn('footer__theme-icon', { 'footer__theme-icon--active': currentValue === theme })}>
<use xlinkHref={content[theme].icon} />
</svg>

View File

@@ -0,0 +1,40 @@
import { test, expect } from '@playwright/test';
import { ADMIN_USERNAME, ADMIN_PASSWORD } from '../constants';
test.describe('Control Panel', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login.html');
await page.getByTestId('username').click();
await page.getByTestId('username').fill(ADMIN_USERNAME);
await page.getByTestId('password').click();
await page.getByTestId('password').fill(ADMIN_PASSWORD);
await page.keyboard.press('Tab');
await page.getByTestId('sign_in').click();
await page.waitForURL((url) => !url.href.endsWith('/login.html'));
});
test('should sign out successfully', async ({ page }) => {
await page.getByTestId('sign_out').click();
await page.waitForURL((url) => url.href.endsWith('/login.html'));
await expect(page.getByTestId('sign_in')).toBeVisible();
});
test('should change theme to dark and then light', async ({ page }) => {
await page.getByTestId('theme_dark').click();
await expect(page.locator('body[data-theme="dark"]')).toBeVisible();
await page.getByTestId('theme_light').click();
await expect(page.locator('body:not([data-theme="dark"])')).toBeVisible();
});
});

View File

@@ -0,0 +1,52 @@
import { test, expect, type Page } from '@playwright/test';
import { ADMIN_USERNAME, ADMIN_PASSWORD } from '../constants';
test.describe('DNS Settings', () => {
test.beforeEach(async ({ page }) => {
// Login before each test
await page.goto('/login.html');
await page.getByTestId('username').click();
await page.getByTestId('username').fill(ADMIN_USERNAME);
await page.getByTestId('password').click();
await page.getByTestId('password').fill(ADMIN_PASSWORD);
await page.keyboard.press('Tab');
await page.getByTestId('sign_in').click();
await page.waitForURL((url) => !url.href.endsWith('/login.html'));
});
const runDNSSettingsTest = async (page: Page, address: string) => {
await page.goto('/#dns');
const currentDns = await page.getByTestId('upstream_dns').inputValue();
await page.getByTestId('upstream_dns').fill(address);
await page.getByTestId('dns_upstream_test').click();
await page.waitForTimeout(2000);
await expect(page.getByTestId('upstream_dns')).toHaveValue(address);
await page.getByTestId('upstream_dns').fill(currentDns);
await page.getByTestId('dns_upstream_save').click({ force: true });
};
test('test for Default DNS', async ({ page }) => {
await runDNSSettingsTest(page, 'https://dns10.quad9.net/dns-query');
});
test('test for Plain DNS', async ({ page }) => {
await runDNSSettingsTest(page, '94.140.14.140');
});
test('test for DNS-over-HTTPS', async ({ page }) => {
await runDNSSettingsTest(page, 'https://unfiltered.adguard-dns.com/dns-query');
});
test('test for DNS-over-TLS', async ({ page }) => {
await runDNSSettingsTest(page, 'tls://unfiltered.adguard-dns.com');
});
test('test for DNS-over-QUIC', async ({ page }) => {
await runDNSSettingsTest(page, 'quic://unfiltered.adguard-dns.com');
});
});

View File

@@ -0,0 +1,73 @@
import { test, expect, type Page } from '@playwright/test';
import { execSync } from 'child_process';
import { ADMIN_USERNAME, ADMIN_PASSWORD } from '../constants';
test.describe('Filtering', () => {
test.beforeEach(async ({ page }) => {
// Login before each test
await page.goto('/login.html');
await page.getByTestId('username').click();
await page.getByTestId('username').fill(ADMIN_USERNAME);
await page.getByTestId('password').click();
await page.getByTestId('password').fill(ADMIN_PASSWORD);
await page.keyboard.press('Tab');
await page.getByTestId('sign_in').click();
await page.waitForURL((url) => !url.href.endsWith('/login.html'));
});
const runTerminalCommand = (command: string) => {
try {
console.info(`Executing command: ${command}`);
const output = execSync(command, { encoding: 'utf-8', stdio: 'pipe' }).trim();
console.info('Command executed successfully.');
console.debug(`Command output:\n${output}`);
return output;
} catch (error: any) {
console.error(`Command execution failed with error:\n${error.message}`);
throw new Error(`Failed to execute command: ${command}\nError: ${error.message}`);
}
}
const runCustomRuleTest = async (page: Page, domain_to_block: string) => {
await page.goto('/#custom_rules');
await page.getByTestId('custom_rule_textarea').fill(domain_to_block);
await page.getByTestId('apply_custom_rule').click();
const nslookupBlockedResult = await runTerminalCommand(`nslookup ${domain_to_block} 127.0.0.1`).toString();
console.info(`nslookup blocked CNAME result: '${nslookupBlockedResult}'`);
const currentRules = await page.getByTestId('custom_rule_textarea').inputValue();
console.debug(`Current rules before removal:\n${currentRules}`);
if (currentRules.includes(domain_to_block)) {
const updatedRules = currentRules
.split('\n')
.filter((line) => line.trim() !== domain_to_block.trim())
.join('\n');
await page.getByTestId('custom_rule_textarea').fill(updatedRules);
console.info(`Rule '${domain_to_block}' removed successfully.`);
console.info('Applying the updated filtering rules after removal.');
await page.getByTestId('apply_custom_rule').click();
await page.waitForLoadState('domcontentloaded');
console.info(`Filtering rules successfully updated after removing '${domain_to_block}'.`);
} else {
console.warn(`Rule '${domain_to_block}' not found. No changes were made.`);
}
const nslookupUnblockedResult = await runTerminalCommand(`nslookup ${domain_to_block} 127.0.0.1`).toString();
console.info(`nslookup unblocked CNAME result: '${nslookupUnblockedResult}'`);
};
test('Test blocking rule for apple.com', async ({ page }) => {
await runCustomRuleTest(page, 'apple.com');
});
});

View File

@@ -0,0 +1,89 @@
import { test, expect } from '@playwright/test';
import { execSync } from 'child_process';
import { ADMIN_USERNAME, ADMIN_PASSWORD } from '../constants';
test.describe('General Settings', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login.html');
await page.getByTestId('username').click();
await page.getByTestId('username').fill(ADMIN_USERNAME);
await page.getByTestId('password').click();
await page.getByTestId('password').fill(ADMIN_PASSWORD);
await page.keyboard.press('Tab');
await page.getByTestId('sign_in').click();
await page.waitForURL((url) => !url.href.endsWith('/login.html'));
});
test('should toggle browsing security feature and verify DNS changes', async ({ page }) => {
await page.goto('/#settings');
const browsingSecurity = await page.getByTestId('safebrowsing');
const browsingSecurityLabel = await browsingSecurity.locator('xpath=following-sibling::*[1]');
const initialState = await browsingSecurity.isChecked();
if (!initialState) {
await browsingSecurityLabel.click();
await expect(browsingSecurity).toBeChecked();
}
const resultEnabled = execSync('nslookup totalvirus.com 127.0.0.1').toString();
await browsingSecurityLabel.click();
await expect(browsingSecurity).not.toBeChecked();
const resultDisabled = execSync('nslookup totalvirus.com 127.0.0.1').toString();
expect(resultEnabled).not.toEqual(resultDisabled);
if (initialState) {
await browsingSecurityLabel.click();
await expect(browsingSecurity).toBeChecked();
}
});
test('should toggle parental control feature and verify DNS changes', async ({ page }) => {
await page.goto('/#settings');
const parentalControl = page.getByTestId('parental');
const parentalControlLabel = await parentalControl.locator('xpath=following-sibling::*[1]');
const initialState = await parentalControl.isChecked();
if (!initialState) {
await parentalControlLabel.click();
await expect(parentalControl).toBeChecked();
}
const resultEnabled = execSync('nslookup pornhub.com 127.0.0.1').toString();
await parentalControlLabel.click();
await expect(parentalControl).not.toBeChecked();
const resultDisabled = execSync('nslookup pornhub.com 127.0.0.1').toString();
expect(resultEnabled).not.toEqual(resultDisabled);
if (initialState) {
await parentalControlLabel.click();
await expect(parentalControl).toBeChecked();
}
});
test('should toggle safe search feature', async ({ page }) => {
await page.goto('/#settings');
const safeSearch = page.getByTestId('safesearch');
const safeSearchLabel = await safeSearch.locator('xpath=following-sibling::*[1]');
const initialState = await safeSearch.isChecked();
await safeSearchLabel.click();
await expect(safeSearch).not.toBeChecked({ checked: initialState });
await safeSearchLabel.click();
await expect(safeSearch).toBeChecked({ checked: initialState });
});
});

View File

@@ -0,0 +1,124 @@
import { test, expect } from '@playwright/test';
import { ADMIN_USERNAME, ADMIN_PASSWORD } from '../constants';
test.describe('QueryLog', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login.html');
await page.getByTestId('username').click();
await page.getByTestId('username').fill(ADMIN_USERNAME);
await page.getByTestId('password').click();
await page.getByTestId('password').fill(ADMIN_PASSWORD);
await page.keyboard.press('Tab');
await page.getByTestId('sign_in').click();
await page.waitForURL((url) => !url.href.endsWith('/login.html'));
});
test('Search of queryLog should work correctly', async ({ page }) => {
await page.route('/control/querylog', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(
{
"data": [
{
"answer": [
{
"type": "A",
"value": "77.88.44.242",
"ttl": 294
},
{
"type": "A",
"value": "5.255.255.242",
"ttl": 294
},
{
"type": "A",
"value": "77.88.55.242",
"ttl": 294
}
],
"answer_dnssec": false,
"cached": false,
"client": "127.0.0.1",
"client_info": {
"whois": {},
"name": "localhost",
"disallowed_rule": "127.0.0.1",
"disallowed": false
},
"client_proto": "",
"elapsedMs": "78.163167",
"question": {
"class": "IN",
"name": "ya.ru",
"type": "A"
},
"reason": "NotFilteredNotFound",
"rules": [],
"status": "NOERROR",
"time": "2024-07-17T16:02:37.500662+02:00",
"upstream": "https://dns10.quad9.net:443/dns-query"
},
{
"answer": [
{
"type": "A",
"value": "77.88.55.242",
"ttl": 351
},
{
"type": "A",
"value": "77.88.44.242",
"ttl": 351
},
{
"type": "A",
"value": "5.255.255.242",
"ttl": 351
}
],
"answer_dnssec": false,
"cached": false,
"client": "127.0.0.1",
"client_info": {
"whois": {},
"name": "localhost",
"disallowed_rule": "127.0.0.1",
"disallowed": false
},
"client_proto": "",
"elapsedMs": "5051.070708",
"question": {
"class": "IN",
"name": "ya.ru",
"type": "A"
},
"reason": "NotFilteredNotFound",
"rules": [],
"status": "NOERROR",
"time": "2024-07-17T16:02:37.4983+02:00",
"upstream": "https://dns10.quad9.net:443/dns-query"
}
],
"oldest": "2024-07-17T16:02:37.4983+02:00"
}
),
});
});
await page.goto('/#logs');
await page.getByTestId('querylog_search').fill('127.0.0.1');
const [request] = await Promise.all([
page.waitForRequest((req) => req.url().includes('/control/querylog')),
]);
if (request) {
expect(request.url()).toContain('search=127.0.0.1');
expect(await page.getByTestId('querylog_cell').first().isVisible()).toBe(true);
}
});
});