Second draft

This commit is contained in:
varjolintu 2022-03-18 16:10:36 +02:00
parent c18cbc630d
commit 97e07b5a62
53 changed files with 190 additions and 981 deletions

View file

@ -1,110 +0,0 @@
'use strict';
function kpxcAssert(func, expected, card, testName) {
if (func === expected) {
createResult(card, true, `Test passed: ${testName}`);
return;
}
createResult(card, false, `Test failed: ${testName}. Result is: ${func}`);
}
function assertRegex(func, expected, card, testName) {
if ((func === null && expected === false)
|| (func && (func.length > 0) === expected)) {
createResult(card, true, `Test passed: ${testName}`);
return;
}
createResult(card, false, `Test failed: ${testName}. Result is: ${func}`);
}
async function assertInputFields(localFile, expectedFieldCount, actionElementId) {
return new Promise((resolve) => {
const iframe = document.getElementById('testFile');
iframe.src = localFile;
const iframeLoaded = function() {
const frameContent = iframe.contentWindow.document.getElementsByTagName('body')[0];
// Load prototypes to iframe. This doesn't work automatically from ui.js
iframe.contentWindow.Element.prototype.getLowerCaseAttribute = function(attr) {
return this.getAttribute(attr) ? this.getAttribute(attr).toLowerCase() : undefined;
};
// An user interaction is required before testing
if (actionElementId) {
const actionElement = frameContent.querySelector(actionElementId);
if (actionElement) {
actionElement.click();
}
}
const inputs = kpxcObserverHelper.getInputs(frameContent);
kpxcAssert(inputs.length, expectedFieldCount, Tests.INPUT_FIELDS, `getInputs() for ${localFile} with ${expectedFieldCount} fields`);
iframe.removeEventListener('load', iframeLoaded);
resolve();
};
// Wait for iframe to load
iframe.addEventListener('load', iframeLoaded);
});
}
async function assertPasswordChangeFields(localFile, expectedNewPassword) {
return new Promise((resolve) => {
const iframe = document.getElementById('testFile');
iframe.src = localFile;
const iframeLoaded = function() {
const frameContent = iframe.contentWindow.document.getElementsByTagName('body')[0];
// Load prototypes to iframe. This doesn't work automatically from ui.js
iframe.contentWindow.Element.prototype.getLowerCaseAttribute = function(attr) {
return this.getAttribute(attr) ? this.getAttribute(attr).toLowerCase() : undefined;
};
const inputs = kpxcObserverHelper.getInputs(frameContent, true);
const newPassword = kpxcForm.getNewPassword(inputs);
kpxcAssert(newPassword, expectedNewPassword, Tests.PASSWORD_CHANGE, `New password matches for ${localFile}`);
iframe.removeEventListener('load', iframeLoaded);
resolve();
};
// Wait for iframe to load
iframe.addEventListener('load', iframeLoaded);
});
}
async function assertTOTPField(classStr, properties, testName, expectedResult) {
const input = kpxcUI.createElement('input', classStr, properties);
document.body.appendChild(input);
const isAccepted = kpxcTOTPIcons.isAcceptedTOTPField(input);
const isValid = kpxcTOTPIcons.isValid(input);
document.body.removeChild(input);
kpxcAssert(isAccepted && isValid, expectedResult, Tests.TOTP_FIELDS, testName);
}
async function assertSearchField(classStr, properties, testName, expectedResult) {
const input = kpxcUI.createElement('input', classStr, properties);
document.body.appendChild(input);
const isSearchfield = kpxcFields.isSearchField(input);
document.body.removeChild(input);
kpxcAssert(isSearchfield, expectedResult, Tests.SEARCH_FIELDS, testName);
}
async function assertSearchForm(properties, testName, expectedResult) {
const form = kpxcUI.createElement('form', '', { action: 'search' });
const input = kpxcUI.createElement('input', '', properties);
form.appendChild(input);
document.body.appendChild(form);
const isSearchfield = kpxcFields.isSearchField(input);
document.body.removeChild(form);
kpxcAssert(isSearchfield, expectedResult, Tests.SEARCH_FIELDS, testName);
}

View file

@ -1,6 +0,0 @@
<html>
<body>
<input placeholder="username" type="text" name="loginField">
<input placeholder="password" type="password" name="passwordField">
</body>
</html>

View file

@ -1,75 +0,0 @@
'use strict';
const { chromium, test, expect } = require('@playwright/test');
const fileUrl = require('file-url');
const DEST = 'keepassxc-browser/tests';
test.beforeEach(async ({ page }) => {
await page.goto(fileUrl(`${DEST}/tests.html`));
});
test.describe('Content script tests', () => {
test('General tests', async ({ page }) => {
const resultCount = await page.locator('css=#general-results >> css=.fa').count();
await expect.soft(resultCount).toBeGreaterThan(0);
for (var i = 0; i < resultCount; i++) {
const elem = await page.locator('css=#general-results >> css=.fa').nth(i);
const id = await elem.getAttribute('id');
await expect.soft(elem, id).toHaveClass('fa fa-check');
}
});
test('Input field matching tests', async ({ page }) => {
const resultCount = await page.locator('css=#input-field-results >> css=.fa').count();
await expect.soft(resultCount).toBeGreaterThan(0);
for (var i = 0; i < resultCount; i++) {
const elem = await page.locator('css=#input-field-results >> css=.fa').nth(i);
const id = await elem.getAttribute('id');
await expect.soft(elem, id).toHaveClass('fa fa-check');
}
});
test('Search field tests', async ({ page }) => {
const resultCount = await page.locator('css=#search-field-results >> css=.fa').count();
await expect.soft(resultCount).toBeGreaterThan(0);
for (var i = 0; i < resultCount; i++) {
const elem = await page.locator('css=#search-field-results >> css=.fa').nth(i);
const id = await elem.getAttribute('id');
await expect.soft(elem, id).toHaveClass('fa fa-check');
}
});
test('TOTP field tests', async ({ page }) => {
const resultCount = await page.locator('css=#totp-field-results >> css=.fa').count();
await expect.soft(resultCount).toBeGreaterThan(0);
for (var i = 0; i < resultCount; i++) {
const elem = await page.locator('css=#totp-field-results >> css=.fa').nth(i);
const id = await elem.getAttribute('id');
await expect.soft(elem, id).toHaveClass('fa fa-check');
}
});
test('Password change tests', async ({ page }) => {
const resultCount = await page.locator('css=#password-change-results >> css=.fa').count();
await expect.soft(resultCount).toBeGreaterThan(0);
for (var i = 0; i < resultCount; i++) {
const elem = await page.locator('css=#password-change-results >> css=.fa').nth(i);
const id = await elem.getAttribute('id');
await expect.soft(elem, id).toHaveClass('fa fa-check');
}
});
});
const verifyResults = async(page, selector) => {
const resultCount = await page.locator(`css=#${selector} >> css=.fa`).count();
const elem = await page.locator(`css=#${selector} >> css=.fa`).nth(0);
const id = await elem.getAttribute('id');
await expect.soft(elem, id).toHaveClass('fa fa-check');
};

View file

@ -1,9 +0,0 @@
const fs = require('fs-extra');
const DEST = 'keepassxc-browser/tests';
module.exports = async config => {
// Create a temporary directory and copy tests/* to keepassxc-browser/tests
await fs.ensureDir(DEST);
await fs.copy('./tests', DEST);
};

View file

@ -1,8 +0,0 @@
const fs = require('fs-extra');
const DEST = 'keepassxc-browser/tests';
module.exports = async config => {
// Delete previously created temporary directory. Comment for re-running tests manually inside the extension.
await fs.remove(DEST);
};

View file

@ -1,6 +0,0 @@
<html>
<body>
<input placeholder="username" type="text" name="loginField">
<input placeholder="password" type="password" name="passwordField">
</body>
</html>

View file

@ -1,5 +0,0 @@
<html>
<body>
<input placeholder="username" type="text" name="loginField">
</body>
</html>

View file

@ -1,5 +0,0 @@
<html>
<body>
<input placeholder="password" type="password" name="passwordField">
</body>
</html>

View file

@ -1,7 +0,0 @@
<html>
<body>
<input placeholder="username" type="text" name="loginField">
<input placeholder="password" type="password" name="passwordField">
<input type="text" id="auth">
</body>
</html>

View file

@ -1,2 +0,0 @@
<input placeholder="username" type="text" name="loginField">
<input placeholder="password" type="password" name="passwordField">

View file

@ -1,14 +0,0 @@
<html>
<head>
<script defer src="div1.js"></script>
</head>
<body>
<button id="toggle">View login form</button>
<div id="loginForm" style="display: none;">
<input placeholder="username" type="text" name="loginField">
<input placeholder="password" type="password" name="passwordField">
</div>
</body>
</html>

View file

@ -1,11 +0,0 @@
'use script';
document.getElementById('toggle').addEventListener('click', function(e) {
const loginForm = document.getElementById('loginForm');
if (loginForm.style.display === 'none') {
loginForm.style.display = 'block';
} else {
loginForm.style.display = 'none';
}
});

View file

@ -1,21 +0,0 @@
<html>
<head>
<script defer src="div2.js"></script>
</head>
<body>
<button id="toggle">View login form</button>
<div id="dialog" style="position: relative; z-index: auto;">
<div id="outer" style="overflow: hidden; height: 0px;">
<div id="inner" style="margin: -197px auto auto;">
<form method="post" class="signin" action="#">
<input placeholder="username" type="text" name="loginField">
<input placeholder="password" type="password" name="passwordField">
</form>
</div>
</div>
</div>
</body>
</html>

View file

@ -1,18 +0,0 @@
'use script';
document.getElementById('toggle').addEventListener('click', function(e) {
const dialog = document.getElementById('dialog');
const outer = document.getElementById('outer');
const inner = document.getElementById('inner');
if (dialog.style.zIndex === 'auto') {
dialog.style.zIndex = 9999;
inner.style.margin = '0px';
outer.style.height = 'auto';
} else {
dialog.style.zIndex = 'auto';
inner.style.margin = '-197px';
outer.style.height = '0px';
}
});

View file

@ -1,10 +0,0 @@
<html>
<head>
<script defer src="div3.js"></script>
</head>
<body>
<button id="toggle">View login form</button>
</body>
</html>

View file

@ -1,10 +0,0 @@
<html>
<head>
<script defer src="div4.js"></script>
</head>
<body>
<button id="toggle">View login form</button>
</body>
</html>

View file

@ -1,12 +0,0 @@
<html>
<body>
<div style="margin-left: -500px;">
<input placeholder="outsideLeft" type="password" name="outsideLeft">
</div>
<div style="margin-top: -500px;">
<input placeholder="outsideTop" type="password" name="outsideTop">
</div>
</body>
</html>

View file

@ -1,18 +0,0 @@
<html>
<style>
.hiddenOne {
visibility: hidden;
}
</style>
<body>
<div>
<input placeholder="zeroSize" type="password" name="zeroSize" style="width: 0px; height: 0px;">
<input placeholder="oneSize" type="password" name="oneSize" style="width: 1px; height: 1px;">
<input placeholder="visibilityHidden" type="password" name="visibilityHidden" style="visibility: hidden;">
<input placeholder="visibilityCollapse" type="password" name="visibilityCollapse" style="visibility: collapse;">
<input placeholder="displayNone" type="password" name="displayNone" style="display: none;">
<input placeholder="hiddenOne" type="password" name="hiddenOne" class="hiddenOne">
<input placeholder="normal" type="password" name="normal">
</div>
</body>
</html>

View file

@ -1,7 +0,0 @@
<html>
<body>
<br /><input type="password" name="Old password" value="oldPassword">
<br /><input type="password" name="New password" value="newPassword">
<br /><input type="password" name="Repeat password" value="newPassword">
</body>
</html>

View file

@ -1,7 +0,0 @@
<html>
<body>
<br /><input type="password" name="New password" value="newPassword">
<br /><input type="password" name="Repeat password" value="newPassword">
<br /><input type="password" name="Old password" value="oldPassword">
</body>
</html>

View file

@ -1,10 +0,0 @@
<html>
<body>
<form action="secondPage">
<br /><input type="password" name="Old password" value="oldPassword">
<br /><input type="password" name="New password" value="newPassword">
<br /><input type="password" name="Repeat password" value="newPassword">
<br /><input type="submit" value="Change password">
</form>
</body>
</html>

View file

@ -1,10 +0,0 @@
<html>
<body>
<form action="secondPage">
<br /><input type="password" name="New password" value="newPassword">
<br /><input type="password" name="Repeat password" value="newPassword">
<br /><input type="password" name="Old password" value="oldPassword">
<br /><input type="submit" value="Change password">
</form>
</body>
</html>

View file

@ -1,17 +0,0 @@
<html>
<body>
<form action="firstForm">
<br /><input type="password" name="Old password" value="oldPassword">
</form>
<form action="secondForm">
<br /><input type="password" name="New password" value="newPassword">
</form>
<form action="thirdForm">
<br /><input type="password" name="Repeat password" value="newPassword">
</form>
</body>
</html>

View file

@ -1,79 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title data-i18n="popupTitle"></title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="../css/colors.css" />
<link rel="stylesheet" href="../bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="../fonts/fork-awesome.min.css" />
<link rel="stylesheet" href="../options/options.css" />
<link rel="icon" type="image/png" href="../icons/keepassxc_32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="../icons/keepassxc_64x64.png" sizes="64x64">
<link rel="icon" type="image/png" href="../icons/keepassxc_96x96.png" sizes="96x96">
<script src="../common/browser-polyfill.min.js"></script>
<script src="../bootstrap/jquery-3.4.1.min.js"></script>
<script src="../bootstrap/bootstrap.min.js"></script>
<script defer src="../common/global.js"></script>
<script defer src="../common/sites.js"></script>
<script defer src="../content/ui.js"></script>
<script defer src="../content/pwgen.js"></script>
<script defer src="../content/define.js"></script>
<script defer src="../content/autocomplete.js"></script>
<script defer src="../content/banner.js"></script>
<script defer src="../content/credential-autocomplete.js"></script>
<script defer src="../content/fields.js"></script>
<script defer src="../content/form.js"></script>
<script defer src="../content/fill.js"></script>
<script defer src="../content/keepassxc-browser.js"></script>-->
<script defer src="../content/observer-helper.js"></script>
<script defer src="../content/totp-autocomplete.js"></script>
<script defer src="../content/totp-field.js"></script>
<script defer src="../content/username-field.js"></script>
<script defer src="assert.js"></script>
<script defer src="tests.js"></script>
</head>
<body class="pt-3 pb-5">
<div class="container-fluid">
<div class="row">
<main class="col-md-9 col-lg-10 offset-md-3 offset-lg-2 px-4 mt-5 mt-md-0">
<div class="content">
<!-- Content script tests -->
<div class="tab">
<h2 class="pb-3 mt-0">Content script tests</h2>
<div class="card my-4 shadow">
<div class="card-header h6 rounded-0"><i class="fa fa-meh-o" aria-hidden="true"></i> General (global.js)</div>
<div class="card-body" id="general-results"></div>
</div>
<div class="card my-4 shadow">
<div class="card-header h6 rounded-0"><i class="fa fa-meh-o" aria-hidden="true"></i> Input field matching (keepassxc-browser.js)</div>
<div class="card-body" id="input-field-results"></div>
</div>
<div class="card my-4 shadow">
<div class="card-header h6 rounded-0"><i class="fa fa-meh-o" aria-hidden="true"></i> Search fields (keepassxc-browser.js)</div>
<div class="card-body" id="search-field-results"></div>
</div>
<div class="card my-4 shadow">
<div class="card-header h6 rounded-0"><i class="fa fa-meh-o" aria-hidden="true"></i> TOTP fields (totp-field.js)</div>
<div class="card-body" id="totp-field-results"></div>
</div>
<div class="card my-4 shadow">
<div class="card-header h6 rounded-0"><i class="fa fa-meh-o" aria-hidden="true"></i> Password change (keepassxc-browser.js)</div>
<div class="card-body" id="password-change-results"></div>
</div>
</div>
<iframe id="testFile" width="100%" height="600" frameborder="0"></iframe>
</main>
</div>
</div>
</body>
</html>

View file

@ -1,168 +0,0 @@
'use strict';
const Tests = {
GENERAL: '#general-results',
INPUT_FIELDS: '#input-field-results',
TOTP_FIELDS: '#totp-field-results',
SEARCH_FIELDS: '#search-field-results',
PASSWORD_CHANGE: '#password-change-results',
};
function createResult(card, res, text) {
const icon = kpxcUI.createElement('i', res ? 'fa fa-check' : 'fa fa-close', { id: text });
const span = kpxcUI.createElement('span', '', '', text);
const br = document.createElement('br');
document.querySelector(card).appendMultiple(icon, span, br);
}
// General (global.js)
async function testGeneral() {
const testCard = Tests.GENERAL;
// General
kpxcAssert(trimURL('https://test.com/path_to_somwhere?login=username'), 'https://test.com/path_to_somwhere', testCard, 'trimURL()');
assertRegex(slashNeededForUrl('https://test.com'), true, testCard, 'slashNeededForUrl()');
assertRegex(slashNeededForUrl('https://test.com/'), false, testCard, 'slashNeededForUrl()');
// URL matching (URL in Site Preferences, page URL, expected result).
// Consider using slighly different URL's for the tests cases.
const matches = [
[ 'https://example.com/*', 'https://example.com/login_page', true ],
[ 'https://example.com/*', 'https://example2.com/login_page', false ],
[ 'https://example.com/*', 'https://subdomain.example.com/login_page', false ],
[ 'https://*.example.com/*', 'https://example.com/login_page', true ],
[ 'https://*.example.com/*', 'https://test.example.com/login_page', true ],
[ 'https://test.example.com/*', 'https://subdomain.example.com/login_page', false ],
[ 'https://test.example.com/page/*', 'https://test.example.com/page/login_page', true ],
[ 'https://test.example.com/page/another_page/*', 'https://test.example.com/page/login', false ],
[ 'https://test.example.com/path/another/a/', 'https://test.example.com/path/another/a/', true ],
[ 'https://test.example.com/path/another/a/', 'https://test.example.com/path/another/b/', false ],
];
for (const m of matches) {
assertRegex(siteMatch(m[0], m[1]), m[2], testCard, `siteMatch() for ${m[1]}`);
}
// Base domain parsing (window.location.hostname)
const domains = [
[ 'another.example.co.uk', 'example.co.uk' ],
[ 'www.example.com', 'example.com' ],
[ 'test.net', 'test.net' ],
[ 'so.many.subdomains.co.jp', 'subdomains.co.jp' ],
[ 'test.site.example.com.au', 'example.com.au' ],
[ '192.168.0.1', '192.168.0.1' ]
];
for (const d of domains) {
kpxcAssert(getTopLevelDomainFromUrl(d[0]), d[1], testCard, 'getBaseDomainFromUrl() for ' + d[0]);
}
}
// Input field matching (keepassxc-browser.js)
async function testInputFields() {
// Local filename, expected fields, action element ID (a button to be clicked)
const localFiles = [
[ 'html/basic1.html', 2 ], // Username/passwd fields
[ 'html/basic2.html', 1 ], // Only username field
[ 'html/basic3.html', 1 ], // Only password field
[ 'html/basic4.html', 3 ], // Username/passwd/TOTP fields
[ 'html/div1.html', 2, '#toggle' ], // Fields are behind a button that must be pressed
[ 'html/div2.html', 2, '#toggle' ], // Fields are behind a button that must be pressed behind a JavaScript
[ 'html/div3.html', 2, '#toggle' ], // Fields are behind a button that must be pressed
[ 'html/div4.html', 2, '#toggle' ], // Fields are behind a button that must be pressed
[ 'html/hidden_fields1.html', 0 ], // Two hidden fields
[ 'html/hidden_fields2.html', 1 ], // Two hidden fields with one visible
];
for (const file of localFiles) {
await assertInputFields(file[0], file[1], file[2]);
}
document.getElementById('testFile').hidden = true;
}
// Search fields (kpxcFields
async function testSearchFields() {
const searchFields = [
[ '', { id: 'otp_field', name: 'otp', type: 'text', maxLength: '8' }, 'Generic 2FA field', false ],
[ '', { placeholder: 'search', type: 'text', id: 'username' }, 'Placeholder only', true ],
[ '', { ariaLabel: 'search', type: 'text', id: 'username' }, 'aria-label only', true ],
];
for (const field of searchFields) {
assertSearchField(field[0], field[1], field[2], field[3]);
}
assertSearchForm({ id: 'username', type: 'text', }, 'Generic input field under search form', true);
}
// TOTP fields (kpxcTOTPIcons)
async function testTotpFields() {
const totpFields = [
[ '', { id: 'otp_field', name: 'otp', type: 'text', maxLength: '8' }, 'Generic 2FA field', true ],
[ '', { id: '2fa', type: 'text', maxLength: '6' }, 'Generic 2FA field', true ],
[ '', { id: '2fa', type: 'text', maxLength: '4' }, 'Ignore if field maxLength too small', false ],
[ '', { id: '2fa', type: 'text', maxLength: '12' }, 'Ignore if field maxLength too long', false ],
[ '', { id: 'username', type: 'text', }, 'Ignore a generic input field', false ],
[ '', { type: 'password', }, 'Ignore a password input field', false ],
[ // Protonmail
'TwoFA-input ng-empty ng-invalid ng-invalid-required ng-valid-minlength ng-valid-maxlength ng-touched',
{ autocapitalize: 'off', autocorrect: 'off', id: 'twoFactorCode', type: 'text', placeholder: 'Two-factor passcode', name: 'twoFactorCode' },
'Protonmail 2FA',
true
],
[ // Nextcloud
'',
{ minlength: '6', maxLength: '10', name: 'challenge', placeholder: 'Authentication code', type: 'tel', },
'Nextcloud 2FA',
true
],
[ // GMail
'whsOnd zHQkBf',
{ autocomplete: 'off', id: 'idvPin', tabindex: '0', name: 'idvPin', pattern: '[0-9 ]*', type: 'tel', spellcheck: 'false' },
'GMail 2FA',
true
],
[ // Live.com
'form-control',
{ autocomplete: 'off', id: 'idTxtBx_SAOTCC_OTC', maxLength: '8', tabindex: '0', name: 'otc', placeholder: 'Code', type: 'tel' },
'Live.com 2FA',
true
],
];
for (const field of totpFields) {
assertTOTPField(field[0], field[1], field[2], field[3]);
}
}
// Password change
async function testPasswordChange() {
// Local filename, expected new password
const localFiles = [
[ 'html/passwordchange1.html', 'newPassword' ], // Default order without form
[ 'html/passwordchange2.html', 'newPassword' ], // Reversed order without form
[ 'html/passwordchange3.html', 'newPassword' ], // Default order with form
[ 'html/passwordchange4.html', 'newPassword' ], // Reversed order with form
[ 'html/passwordchange5.html', 'newPassword' ], // Each field has own form
];
for (const file of localFiles) {
await assertPasswordChangeFields(file[0], file[1]);
}
document.getElementById('testFile').hidden = true;
}
// Run tests
(async () => {
await Promise.all([
await testGeneral(),
await testInputFields(),
await testSearchFields(),
await testTotpFields(),
await testPasswordChange(),
]);
})();

View file

@ -16,7 +16,7 @@
},
"scripts": {
"build": "node build.js",
"tests": "npx playwright test"
"tests": "npx playwright test tests/tests.js"
},
"repository": {
"type": "git",

View file

@ -8,7 +8,7 @@ const config = {
},
forbidOnly: !!process.env.CI,
globalSetup: require.resolve('./tests/global-setup'),
//globalTeardown: require.resolve('./tests/global-teardown'),
globalTeardown: require.resolve('./tests/global-teardown'),
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'list',
@ -23,12 +23,12 @@ const config = {
...devices['Desktop Chrome'],
},
},
/*{
{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
},
},*/
},
],
};

View file

@ -19,60 +19,38 @@ function assertRegex(func, expected, card, testName) {
createResult(card, false, `Test failed: ${testName}. Result is: ${func}`);
}
async function assertInputFields(localFile, expectedFieldCount, actionElementId) {
return new Promise((resolve) => {
const iframe = document.getElementById('testFile');
iframe.src = localFile;
async function assertInputFields(localDiv, expectedFieldCount, actionElementId) {
return new Promise(async (resolve) => {
const div = document.getElementById(localDiv);
div.style.display = 'block';
const iframeLoaded = function() {
const frameContent = iframe.contentWindow.document.getElementsByTagName('body')[0];
// Load prototypes to iframe. This doesn't work automatically from ui.js
iframe.contentWindow.Element.prototype.getLowerCaseAttribute = function(attr) {
return this.getAttribute(attr) ? this.getAttribute(attr).toLowerCase() : undefined;
};
// An user interaction is required before testing
if (actionElementId) {
const actionElement = frameContent.querySelector(actionElementId);
if (actionElement) {
actionElement.click();
}
// An user interaction is required before testing
if (actionElementId) {
const actionElement = div.querySelector(actionElementId);
if (actionElement) {
actionElement.click();
}
}
const inputs = kpxcObserverHelper.getInputs(frameContent);
kpxcAssert(inputs.length, expectedFieldCount, Tests.INPUT_FIELDS, `getInputs() for ${localFile} with ${expectedFieldCount} fields`);
iframe.removeEventListener('load', iframeLoaded);
resolve();
};
const inputs = kpxcObserverHelper.getInputs(div);
kpxcAssert(inputs.length, expectedFieldCount, Tests.INPUT_FIELDS, `getInputs() for ${localDiv} with ${expectedFieldCount} fields`);
// Wait for iframe to load
iframe.addEventListener('load', iframeLoaded);
div.style.display = 'none';
resolve();
});
}
async function assertPasswordChangeFields(localFile, expectedNewPassword) {
return new Promise((resolve) => {
const iframe = document.getElementById('testFile');
iframe.src = localFile;
async function assertPasswordChangeFields(localDiv, expectedNewPassword) {
return new Promise(async (resolve) => {
const div = document.getElementById(localDiv);
div.style.display = 'block';
const iframeLoaded = function() {
const frameContent = iframe.contentWindow.document.getElementsByTagName('body')[0];
const inputs = kpxcObserverHelper.getInputs(div, true);
const newPassword = kpxcForm.getNewPassword(inputs);
kpxcAssert(newPassword, expectedNewPassword, Tests.PASSWORD_CHANGE, `New password matches for ${localDiv}`);
// Load prototypes to iframe. This doesn't work automatically from ui.js
iframe.contentWindow.Element.prototype.getLowerCaseAttribute = function(attr) {
return this.getAttribute(attr) ? this.getAttribute(attr).toLowerCase() : undefined;
};
const inputs = kpxcObserverHelper.getInputs(frameContent, true);
const newPassword = kpxcForm.getNewPassword(inputs);
kpxcAssert(newPassword, expectedNewPassword, Tests.PASSWORD_CHANGE, `New password matches for ${localFile}`);
iframe.removeEventListener('load', iframeLoaded);
resolve();
};
// Wait for iframe to load
iframe.addEventListener('load', iframeLoaded);
div.style.display = 'none';
resolve();
});
}

View file

@ -1,6 +1,6 @@
'use strict';
const { chromium, test, expect } = require('@playwright/test');
const { test, expect } = require('@playwright/test');
const fileUrl = require('file-url');
const DEST = 'keepassxc-browser/tests';

View file

@ -1,6 +0,0 @@
<html>
<body>
<input placeholder="username" type="text" name="loginField">
<input placeholder="password" type="password" name="passwordField">
</body>
</html>

View file

@ -1,5 +0,0 @@
<html>
<body>
<input placeholder="username" type="text" name="loginField">
</body>
</html>

View file

@ -1,5 +0,0 @@
<html>
<body>
<input placeholder="password" type="password" name="passwordField">
</body>
</html>

View file

@ -1,7 +0,0 @@
<html>
<body>
<input placeholder="username" type="text" name="loginField">
<input placeholder="password" type="password" name="passwordField">
<input type="text" id="auth">
</body>
</html>

View file

@ -1,2 +0,0 @@
<input placeholder="username" type="text" name="loginField">
<input placeholder="password" type="password" name="passwordField">

View file

@ -1,14 +0,0 @@
<html>
<head>
<script defer src="div1.js"></script>
</head>
<body>
<button id="toggle">View login form</button>
<div id="loginForm" style="display: none;">
<input placeholder="username" type="text" name="loginField">
<input placeholder="password" type="password" name="passwordField">
</div>
</body>
</html>

View file

@ -1,21 +0,0 @@
<html>
<head>
<script defer src="div2.js"></script>
</head>
<body>
<button id="toggle">View login form</button>
<div id="dialog" style="position: relative; z-index: auto;">
<div id="outer" style="overflow: hidden; height: 0px;">
<div id="inner" style="margin: -197px auto auto;">
<form method="post" class="signin" action="#">
<input placeholder="username" type="text" name="loginField">
<input placeholder="password" type="password" name="passwordField">
</form>
</div>
</div>
</div>
</body>
</html>

View file

@ -1,10 +0,0 @@
<html>
<head>
<script defer src="div3.js"></script>
</head>
<body>
<button id="toggle">View login form</button>
</body>
</html>

View file

@ -1,26 +0,0 @@
'use script';
document.getElementById('toggle').addEventListener('click', function(e) {
const loginForm = document.getElementById('loginForm');
if (!loginForm) {
const dialog = document.createElement('div');
dialog.setAttribute('id', 'loginForm');
const usernameInput = document.createElement('input');
usernameInput.setAttribute('type', 'text');
usernameInput.setAttribute('name', 'loginField');
usernameInput.setAttribute('placeholder', 'username');
dialog.append(usernameInput);
const passwordInput = document.createElement('input');
passwordInput.setAttribute('type', 'password');
passwordInput.setAttribute('name', 'passwordField');
passwordInput.setAttribute('placeholder', 'password');
dialog.append(passwordInput);
document.body.appendChild(dialog);
} else {
document.body.removeChild(loginForm);
}
});

View file

@ -1,10 +0,0 @@
<html>
<head>
<script defer src="div4.js"></script>
</head>
<body>
<button id="toggle">View login form</button>
</body>
</html>

View file

@ -1,55 +0,0 @@
'use script';
document.getElementById('toggle').addEventListener('click', function(e) {
const loginForm = document.getElementById('loginForm');
if (!loginForm) {
const dialog = document.createElement('div');
dialog.setAttribute('id', 'loginForm');
dialog.style.position = 'fixed';
dialog.style.zIndex = '1002';
const wrapper = document.createElement('div');
wrapper.setAttribute('tabIndex', '-1');
wrapper.style.height = '100%';
wrapper.style.width = '100%';
wrapper.style.outline = '0px';
wrapper.style.overflow = 'visible';
const innerDiv = document.createElement('div');
innerDiv.setAttribute('id', 'innerDiv');
const contentDiv = document.createElement('div');
contentDiv.setAttribute('id', 'contentDiv');
const form = document.createElement('form');
form.setAttribute('action', 'loginUser');
form.setAttribute('method', 'post');
const divUsernameWithLabel = document.createElement('div');
const divPasswordWithLabel = document.createElement('div');
const usernameInput = document.createElement('input');
usernameInput.setAttribute('type', 'text');
usernameInput.setAttribute('name', 'loginField');
usernameInput.setAttribute('placeholder', 'username');
divUsernameWithLabel.append(usernameInput);
const passwordInput = document.createElement('input');
passwordInput.setAttribute('type', 'password');
passwordInput.setAttribute('name', 'passwordField');
passwordInput.setAttribute('placeholder', 'password');
divPasswordWithLabel.append(passwordInput);
form.append(divUsernameWithLabel);
form.append(divPasswordWithLabel);
contentDiv.append(form);
innerDiv.append(contentDiv);
wrapper.append(innerDiv);
dialog.append(wrapper);
document.body.appendChild(dialog);
} else {
document.body.removeChild(loginForm);
}
});

View file

@ -1,12 +0,0 @@
<html>
<body>
<div style="margin-left: -500px;">
<input placeholder="outsideLeft" type="password" name="outsideLeft">
</div>
<div style="margin-top: -500px;">
<input placeholder="outsideTop" type="password" name="outsideTop">
</div>
</body>
</html>

View file

@ -1,18 +0,0 @@
<html>
<style>
.hiddenOne {
visibility: hidden;
}
</style>
<body>
<div>
<input placeholder="zeroSize" type="password" name="zeroSize" style="width: 0px; height: 0px;">
<input placeholder="oneSize" type="password" name="oneSize" style="width: 1px; height: 1px;">
<input placeholder="visibilityHidden" type="password" name="visibilityHidden" style="visibility: hidden;">
<input placeholder="visibilityCollapse" type="password" name="visibilityCollapse" style="visibility: collapse;">
<input placeholder="displayNone" type="password" name="displayNone" style="display: none;">
<input placeholder="hiddenOne" type="password" name="hiddenOne" class="hiddenOne">
<input placeholder="normal" type="password" name="normal">
</div>
</body>
</html>

View file

@ -1,7 +0,0 @@
<html>
<body>
<br /><input type="password" name="Old password" value="oldPassword">
<br /><input type="password" name="New password" value="newPassword">
<br /><input type="password" name="Repeat password" value="newPassword">
</body>
</html>

View file

@ -1,7 +0,0 @@
<html>
<body>
<br /><input type="password" name="New password" value="newPassword">
<br /><input type="password" name="Repeat password" value="newPassword">
<br /><input type="password" name="Old password" value="oldPassword">
</body>
</html>

View file

@ -1,10 +0,0 @@
<html>
<body>
<form action="secondPage">
<br /><input type="password" name="Old password" value="oldPassword">
<br /><input type="password" name="New password" value="newPassword">
<br /><input type="password" name="Repeat password" value="newPassword">
<br /><input type="submit" value="Change password">
</form>
</body>
</html>

View file

@ -1,10 +0,0 @@
<html>
<body>
<form action="secondPage">
<br /><input type="password" name="New password" value="newPassword">
<br /><input type="password" name="Repeat password" value="newPassword">
<br /><input type="password" name="Old password" value="oldPassword">
<br /><input type="submit" value="Change password">
</form>
</body>
</html>

View file

@ -1,17 +0,0 @@
<html>
<body>
<form action="firstForm">
<br /><input type="password" name="Old password" value="oldPassword">
</form>
<form action="secondForm">
<br /><input type="password" name="New password" value="newPassword">
</form>
<form action="thirdForm">
<br /><input type="password" name="Repeat password" value="newPassword">
</form>
</body>
</html>

View file

@ -1,7 +1,7 @@
'use script';
document.getElementById('toggle').addEventListener('click', function(e) {
const loginForm = document.getElementById('loginForm');
document.getElementById('toggle1').addEventListener('click', function(e) {
const loginForm = document.getElementById('loginForm1');
if (loginForm.style.display === 'none') {
loginForm.style.display = 'block';

View file

@ -1,6 +1,6 @@
'use script';
document.getElementById('toggle').addEventListener('click', function(e) {
document.getElementById('toggle2').addEventListener('click', function(e) {
const dialog = document.getElementById('dialog');
const outer = document.getElementById('outer');
const inner = document.getElementById('inner');
@ -9,7 +9,6 @@ document.getElementById('toggle').addEventListener('click', function(e) {
dialog.style.zIndex = 9999;
inner.style.margin = '0px';
outer.style.height = 'auto';
} else {
dialog.style.zIndex = 'auto';
inner.style.margin = '-197px';

View file

@ -1,6 +1,6 @@
'use script';
document.getElementById('toggle').addEventListener('click', function(e) {
document.getElementById('toggle3').addEventListener('click', function(e) {
const loginForm = document.getElementById('loginForm');
if (!loginForm) {
@ -19,8 +19,6 @@ document.getElementById('toggle').addEventListener('click', function(e) {
passwordInput.setAttribute('placeholder', 'password');
dialog.append(passwordInput);
document.body.appendChild(dialog);
} else {
document.body.removeChild(loginForm);
e.currentTarget.parentElement.appendChild(dialog);
}
});

View file

@ -1,11 +1,11 @@
'use script';
document.getElementById('toggle').addEventListener('click', function(e) {
const loginForm = document.getElementById('loginForm');
document.getElementById('toggle4').addEventListener('click', function(e) {
const loginForm = document.getElementById('loginForm4');
if (!loginForm) {
const dialog = document.createElement('div');
dialog.setAttribute('id', 'loginForm');
dialog.setAttribute('id', 'loginForm4');
dialog.style.position = 'fixed';
dialog.style.zIndex = '1002';
@ -48,8 +48,6 @@ document.getElementById('toggle').addEventListener('click', function(e) {
wrapper.append(innerDiv);
dialog.append(wrapper);
document.body.appendChild(dialog);
} else {
document.body.removeChild(loginForm);
e.currentTarget.parentElement.appendChild(dialog);
}
});

View file

@ -12,8 +12,6 @@
<link rel="icon" type="image/png" href="../icons/keepassxc_64x64.png" sizes="64x64">
<link rel="icon" type="image/png" href="../icons/keepassxc_96x96.png" sizes="96x96">
<script src="../common/browser-polyfill.min.js"></script>
<script src="../bootstrap/jquery-3.4.1.min.js"></script>
<script src="../bootstrap/bootstrap.min.js"></script>
<script defer src="../common/global.js"></script>
<script defer src="../common/sites.js"></script>
<script defer src="../content/ui.js"></script>
@ -34,6 +32,12 @@
<script defer src="tests.js"></script>
</head>
<style>
.hiddenOne {
visibility: hidden;
}
</style>
<body class="pt-3 pb-5">
<div class="container-fluid">
<div class="row">
@ -71,9 +75,132 @@
</div>
<iframe id="testFile" width="100%" height="600" frameborder="0"></iframe>
<!-- Test content -->
<!-- =================================== -->
<div id="basic1" style="display: none;">
<input placeholder="username" type="text" name="loginField">
<input placeholder="password" type="password" name="passwordField">
</div>
<div id="basic2" style="display: none;">
<input placeholder="username" type="text" name="loginField">
</div>
<div id="basic3" style="display: none;">
<input placeholder="password" type="password" name="passwordField">
</div>
<div id="basic4" style="display: none;">
<input placeholder="username" type="text" name="loginField">
<input placeholder="password" type="password" name="passwordField">
<input type="text" id="auth">
</div>
<div id="div1" style="display: none;">
<button id="toggle1">View login form</button>
<div id="loginForm1" style="display: none;">
<input placeholder="username" type="text" name="loginField">
<input placeholder="password" type="password" name="passwordField">
</div>
</div>
<div id="div2" style="display: none;">
<button id="toggle2">View login form</button>
<div id="dialog" style="position: relative; z-index: auto;">
<div id="outer" style="overflow: hidden; height: 0px;">
<div id="inner" style="margin: -197px auto auto;">
<form method="post" class="signin" action="#">
<input placeholder="username" type="text" name="loginField">
<input placeholder="password" type="password" name="passwordField">
</form>
</div>
</div>
</div>
</div>
<div id="div3" style="display: none;">
<button id="toggle3">View login form</button>
</div>
<div id="div4" style="display: none;">
<button id="toggle4">View login form</button>
</div>
<div id="hiddenFields1" style="display: none;">
<div style="position: absolute; margin-left: -500px;">
<input placeholder="outsideLeft" type="password" name="outsideLeft">
</div>
<div style="position: absolute; margin-top: -1500px;">
<input placeholder="outsideTop" type="password" name="outsideTop">
</div>
</div>
<div id="hiddenFields2" style="display: none;">
<div>
<input placeholder="zeroSize" type="password" name="zeroSize" style="width: 0px; height: 0px;">
<input placeholder="oneSize" type="password" name="oneSize" style="width: 1px; height: 1px;">
<input placeholder="visibilityHidden" type="password" name="visibilityHidden" style="visibility: hidden;">
<input placeholder="visibilityCollapse" type="password" name="visibilityCollapse" style="visibility: collapse;">
<input placeholder="displayNone" type="password" name="displayNone" style="display: none;">
<input placeholder="hiddenOne" type="password" name="hiddenOne" class="hiddenOne">
<input placeholder="normal" type="password" name="normal">
</div>
</div>
<div id="passwordChange1" style="display: none;">
<br /><input type="password" name="Old password" value="oldPassword">
<br /><input type="password" name="New password" value="newPassword">
<br /><input type="password" name="Repeat password" value="newPassword">
</div>
<div id="passwordChange2" style="display: none;">
<br /><input type="password" name="New password" value="newPassword">
<br /><input type="password" name="Repeat password" value="newPassword">
<br /><input type="password" name="Old password" value="oldPassword">
</div>
<div id="passwordChange3" style="display: none;">
<form action="secondPage">
<br /><input type="password" name="Old password" value="oldPassword">
<br /><input type="password" name="New password" value="newPassword">
<br /><input type="password" name="Repeat password" value="newPassword">
<br /><input type="submit" value="Change password">
</form>
</div>
<div id="passwordChange4" style="display: none;">
<form action="secondPage">
<br /><input type="password" name="New password" value="newPassword">
<br /><input type="password" name="Repeat password" value="newPassword">
<br /><input type="password" name="Old password" value="oldPassword">
<br /><input type="submit" value="Change password">
</form>
</div>
<div id="passwordChange5" style="display: none;">
<form action="firstForm">
<br /><input type="password" name="Old password" value="oldPassword">
</form>
<form action="secondForm">
<br /><input type="password" name="New password" value="newPassword">
</form>
<form action="thirdForm">
<br /><input type="password" name="Repeat password" value="newPassword">
</form>
</div>
</main>
</div>
</div>
<!-- These need to be loaded only after everything else -->
<script src="scripts/div1.js"></script>
<script src="scripts/div2.js"></script>
<script src="scripts/div3.js"></script>
<script src="scripts/div4.js"></script>
</body>
</html>

View file

@ -61,25 +61,23 @@ async function testGeneral() {
// Input field matching (keepassxc-browser.js)
async function testInputFields() {
// Local filename, expected fields, action element ID (a button to be clicked)
const localFiles = [
[ 'html/basic1.html', 2 ], // Username/passwd fields
[ 'html/basic2.html', 1 ], // Only username field
[ 'html/basic3.html', 1 ], // Only password field
[ 'html/basic4.html', 3 ], // Username/passwd/TOTP fields
[ 'html/div1.html', 2, '#toggle' ], // Fields are behind a button that must be pressed
[ 'html/div2.html', 2, '#toggle' ], // Fields are behind a button that must be pressed behind a JavaScript
[ 'html/div3.html', 2, '#toggle' ], // Fields are behind a button that must be pressed
[ 'html/div4.html', 2, '#toggle' ], // Fields are behind a button that must be pressed
[ 'html/hidden_fields1.html', 0 ], // Two hidden fields
[ 'html/hidden_fields2.html', 1 ], // Two hidden fields with one visible
// Div ID, expected fields, action element ID (a button to be clicked)
const testDivs = [
[ 'basic1', 2 ], // Username/passwd fields
[ 'basic2', 1 ], // Only username field
[ 'basic3', 1 ], // Only password field
[ 'basic4', 3 ], // Username/passwd/TOTP fields
[ 'div1', 2, '#toggle1' ], // Fields are behind a button that must be pressed
[ 'div2', 2, '#toggle2' ], // Fields are behind a button that must be pressed behind a JavaScript
[ 'div3', 2, '#toggle3' ], // Fields are behind a button that must be pressed
[ 'div4', 2, '#toggle4' ], // Fields are behind a button that must be pressed
[ 'hiddenFields1', 0 ], // Two hidden fields
[ 'hiddenFields2', 1 ], // Two hidden fields with one visible
];
for (const file of localFiles) {
await assertInputFields(file[0], file[1], file[2]);
for (const div of testDivs) {
await assertInputFields(div[0], div[1], div[2]);
}
document.getElementById('testFile').hidden = true;
}
// Search fields (kpxcFields
@ -140,20 +138,18 @@ async function testTotpFields() {
// Password change
async function testPasswordChange() {
// Local filename, expected new password
const localFiles = [
[ 'html/passwordchange1.html', 'newPassword' ], // Default order without form
[ 'html/passwordchange2.html', 'newPassword' ], // Reversed order without form
[ 'html/passwordchange3.html', 'newPassword' ], // Default order with form
[ 'html/passwordchange4.html', 'newPassword' ], // Reversed order with form
[ 'html/passwordchange5.html', 'newPassword' ], // Each field has own form
// Div ID, expected new password
const localDivs = [
[ 'passwordChange1', 'newPassword' ], // Default order without form
[ 'passwordChange2', 'newPassword' ], // Reversed order without form
[ 'passwordChange3', 'newPassword' ], // Default order with form
[ 'passwordChange4', 'newPassword' ], // Reversed order with form
[ 'passwordChange5', 'newPassword' ], // Each field has own form
];
for (const file of localFiles) {
await assertPasswordChangeFields(file[0], file[1]);
for (const div of localDivs) {
await assertPasswordChangeFields(div[0], div[1]);
}
document.getElementById('testFile').hidden = true;
}
// Run tests