Fix Kiro password field detection
This commit is contained in:
+169
-24
@@ -41,22 +41,10 @@ const KIRO_OTP_INPUT_SELECTOR = [
|
||||
'input[name*="code" i]',
|
||||
].join(', ');
|
||||
|
||||
const KIRO_PASSWORD_INPUT_SELECTOR = [
|
||||
'input[placeholder="Enter password"]',
|
||||
'input[placeholder*="Create password" i]',
|
||||
'input[placeholder*="Password" i]',
|
||||
'input[name="password"]',
|
||||
'input[id*="password" i]',
|
||||
'input[type="password"]',
|
||||
].join(', ');
|
||||
|
||||
const KIRO_CONFIRM_PASSWORD_SELECTOR = [
|
||||
'input[placeholder*="Re-enter password" i]',
|
||||
'input[placeholder*="Confirm password" i]',
|
||||
'input[placeholder*="Confirm Password" i]',
|
||||
'input[name="confirmPassword"]',
|
||||
'input[id*="confirm" i]',
|
||||
].join(', ');
|
||||
const KIRO_PASSWORD_FIELD_SELECTOR = 'input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]):not([type="submit"]):not([type="button"]):not([type="file"])';
|
||||
const KIRO_PASSWORD_TEXT_PATTERN = /password|\u5bc6\u7801/i;
|
||||
const KIRO_CONFIRM_PASSWORD_TEXT_PATTERN = /confirm\s*password|re[-\s]*enter\s*password|repeat\s*password|verify\s*password|\u786e\u8ba4\s*\u5bc6\u7801|\u518d\u6b21.*\u5bc6\u7801|\u91cd\u590d.*\u5bc6\u7801/i;
|
||||
const KIRO_PRIMARY_PASSWORD_HINT_PATTERN = /enter\s*password|create\s*password|new\s*password|\u8f93\u5165.*\u5bc6\u7801|\u521b\u5efa.*\u5bc6\u7801|^\s*\u5bc6\u7801\s*$/i;
|
||||
|
||||
function isVisibleKiroElement(element) {
|
||||
if (!element) return false;
|
||||
@@ -190,12 +178,156 @@ function findVisibleOtpInput() {
|
||||
return findFirstVisible(KIRO_OTP_INPUT_SELECTOR);
|
||||
}
|
||||
|
||||
function getKiroElementText(element) {
|
||||
return String(element?.textContent || '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function cssEscapeKiroValue(value = '') {
|
||||
if (globalThis.CSS?.escape) {
|
||||
return globalThis.CSS.escape(String(value));
|
||||
}
|
||||
return String(value).replace(/["\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
function getKiroInputLabelText(input) {
|
||||
const labels = [];
|
||||
for (const label of Array.from(input?.labels || [])) {
|
||||
const text = getKiroElementText(label);
|
||||
if (text) labels.push(text);
|
||||
}
|
||||
|
||||
const id = String(input?.id || '').trim();
|
||||
if (id) {
|
||||
for (const label of Array.from(document.querySelectorAll(`label[for="${cssEscapeKiroValue(id)}"]`))) {
|
||||
const text = getKiroElementText(label);
|
||||
if (text) labels.push(text);
|
||||
}
|
||||
}
|
||||
|
||||
const labelledBy = String(input?.getAttribute?.('aria-labelledby') || '').trim();
|
||||
if (labelledBy) {
|
||||
for (const labelId of labelledBy.split(/\s+/).filter(Boolean)) {
|
||||
const label = document.getElementById?.(labelId);
|
||||
const text = getKiroElementText(label);
|
||||
if (text) labels.push(text);
|
||||
}
|
||||
}
|
||||
|
||||
const parentLabel = input?.closest?.('label');
|
||||
const parentLabelText = getKiroElementText(parentLabel);
|
||||
if (parentLabelText) labels.push(parentLabelText);
|
||||
|
||||
return Array.from(new Set(labels)).join(' ');
|
||||
}
|
||||
|
||||
function getKiroPasswordInputContext(input) {
|
||||
const describedBy = String(input?.getAttribute?.('aria-describedby') || '').trim();
|
||||
const describedByText = describedBy
|
||||
? describedBy
|
||||
.split(/\s+/)
|
||||
.map((id) => getKiroElementText(document.getElementById?.(id)))
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
: '';
|
||||
|
||||
return [
|
||||
input?.getAttribute?.('placeholder'),
|
||||
input?.getAttribute?.('name'),
|
||||
input?.id,
|
||||
input?.getAttribute?.('aria-label'),
|
||||
input?.getAttribute?.('data-testid'),
|
||||
input?.getAttribute?.('autocomplete'),
|
||||
getKiroInputLabelText(input),
|
||||
describedByText,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function createKiroPasswordCandidate(input, index) {
|
||||
const context = getKiroPasswordInputContext(input);
|
||||
const type = String(input?.getAttribute?.('type') || input?.type || '').trim().toLowerCase();
|
||||
const isConfirm = KIRO_CONFIRM_PASSWORD_TEXT_PATTERN.test(context);
|
||||
const hasPasswordHint = KIRO_PASSWORD_TEXT_PATTERN.test(context) || type === 'password';
|
||||
if (!hasPasswordHint && !isConfirm) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let primaryScore = 0;
|
||||
let confirmScore = 0;
|
||||
if (type === 'password') primaryScore += 5;
|
||||
if (KIRO_PASSWORD_TEXT_PATTERN.test(context)) primaryScore += 20;
|
||||
if (KIRO_PRIMARY_PASSWORD_HINT_PATTERN.test(context)) primaryScore += 40;
|
||||
if (isConfirm) {
|
||||
primaryScore -= 100;
|
||||
confirmScore += 100;
|
||||
}
|
||||
if (/^password$/i.test(String(input?.getAttribute?.('name') || ''))) primaryScore += 40;
|
||||
if (/^password$/i.test(String(input?.id || ''))) primaryScore += 20;
|
||||
|
||||
return {
|
||||
input,
|
||||
index,
|
||||
context,
|
||||
isConfirm,
|
||||
primaryScore,
|
||||
confirmScore,
|
||||
};
|
||||
}
|
||||
|
||||
function findKiroPasswordFields() {
|
||||
const candidates = collectVisibleElements(KIRO_PASSWORD_FIELD_SELECTOR)
|
||||
.map((input, index) => createKiroPasswordCandidate(input, index))
|
||||
.filter(Boolean);
|
||||
if (!candidates.length) {
|
||||
return {
|
||||
passwordInput: null,
|
||||
confirmPasswordInput: null,
|
||||
passwordInputs: [],
|
||||
};
|
||||
}
|
||||
|
||||
const confirmCandidate = candidates
|
||||
.filter((candidate) => candidate.isConfirm)
|
||||
.sort((left, right) => right.confirmScore - left.confirmScore || left.index - right.index)[0] || null;
|
||||
|
||||
let passwordCandidate = candidates
|
||||
.filter((candidate) => !candidate.isConfirm)
|
||||
.sort((left, right) => right.primaryScore - left.primaryScore || left.index - right.index)[0] || null;
|
||||
|
||||
if (!passwordCandidate) {
|
||||
passwordCandidate = candidates
|
||||
.filter((candidate) => !confirmCandidate || candidate.input !== confirmCandidate.input)
|
||||
.sort((left, right) => left.index - right.index)[0] || null;
|
||||
}
|
||||
|
||||
let resolvedConfirmCandidate = confirmCandidate;
|
||||
if (
|
||||
(!resolvedConfirmCandidate || resolvedConfirmCandidate.input === passwordCandidate?.input)
|
||||
&& passwordCandidate
|
||||
) {
|
||||
resolvedConfirmCandidate = candidates
|
||||
.filter((candidate) => candidate.input !== passwordCandidate.input)
|
||||
.sort((left, right) => left.index - right.index)[0] || null;
|
||||
}
|
||||
|
||||
return {
|
||||
passwordInput: passwordCandidate?.input || null,
|
||||
confirmPasswordInput: resolvedConfirmCandidate?.input || null,
|
||||
passwordInputs: candidates.map((candidate) => candidate.input),
|
||||
};
|
||||
}
|
||||
|
||||
function findVisiblePasswordInputs() {
|
||||
return collectVisibleElements(KIRO_PASSWORD_INPUT_SELECTOR);
|
||||
return findKiroPasswordFields().passwordInputs;
|
||||
}
|
||||
|
||||
function findVisibleConfirmPasswordInput() {
|
||||
return findFirstVisible(KIRO_CONFIRM_PASSWORD_SELECTOR);
|
||||
return findKiroPasswordFields().confirmPasswordInput;
|
||||
}
|
||||
|
||||
function findEmailContinueButton(emailInput = null) {
|
||||
@@ -378,15 +510,14 @@ function detectKiroRegisterPageState() {
|
||||
}
|
||||
}
|
||||
|
||||
const passwordInputs = findVisiblePasswordInputs();
|
||||
const confirmPasswordInput = findVisibleConfirmPasswordInput();
|
||||
if (passwordInputs.length) {
|
||||
const passwordInput = passwordInputs[0];
|
||||
const passwordFields = findKiroPasswordFields();
|
||||
if (passwordFields.passwordInput) {
|
||||
const passwordInput = passwordFields.passwordInput;
|
||||
return {
|
||||
state: 'password_page',
|
||||
url: currentUrl,
|
||||
passwordInput,
|
||||
confirmPasswordInput,
|
||||
confirmPasswordInput: passwordFields.confirmPasswordInput,
|
||||
continueButton: findPasswordContinueButton(passwordInput),
|
||||
};
|
||||
}
|
||||
@@ -645,15 +776,29 @@ async function submitKiroPassword(payload = {}) {
|
||||
|
||||
fillInput(readyState.passwordInput, password);
|
||||
await sleep(150);
|
||||
if (String(readyState.passwordInput.value || '') !== password) {
|
||||
throw new Error('Kiro \u5bc6\u7801\u9875\u4e3b\u5bc6\u7801\u6846\u586b\u5165\u5931\u8d25\uff0c\u5df2\u505c\u6b62\u63d0\u4ea4\uff0c\u8bf7\u68c0\u67e5\u9875\u9762\u5b57\u6bb5\u7ed3\u6784\u3002');
|
||||
}
|
||||
|
||||
const confirmPasswordInput = readyState.confirmPasswordInput
|
||||
|| (() => {
|
||||
const passwordInputs = findVisiblePasswordInputs();
|
||||
return passwordInputs.length > 1 ? passwordInputs[1] : null;
|
||||
return passwordInputs.find((input) => input !== readyState.passwordInput) || null;
|
||||
})();
|
||||
if (confirmPasswordInput && confirmPasswordInput !== readyState.passwordInput) {
|
||||
fillInput(confirmPasswordInput, password);
|
||||
await sleep(150);
|
||||
if (String(confirmPasswordInput.value || '') !== password) {
|
||||
throw new Error('Kiro \u5bc6\u7801\u9875\u786e\u8ba4\u5bc6\u7801\u6846\u586b\u5165\u5931\u8d25\uff0c\u5df2\u505c\u6b62\u63d0\u4ea4\uff0c\u8bf7\u68c0\u67e5\u9875\u9762\u5b57\u6bb5\u7ed3\u6784\u3002');
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
confirmPasswordInput
|
||||
&& confirmPasswordInput !== readyState.passwordInput
|
||||
&& String(confirmPasswordInput.value || '') !== String(readyState.passwordInput.value || '')
|
||||
) {
|
||||
throw new Error('Kiro \u5bc6\u7801\u9875\u4e24\u4e2a\u5bc6\u7801\u6846\u5185\u5bb9\u4e0d\u4e00\u81f4\uff0c\u5df2\u505c\u6b62\u63d0\u4ea4\u3002');
|
||||
}
|
||||
|
||||
simulateClick(readyState.continueButton);
|
||||
|
||||
@@ -5,7 +5,86 @@ const vm = require('node:vm');
|
||||
|
||||
const source = fs.readFileSync('content/kiro/register-page.js', 'utf8');
|
||||
|
||||
function createTextNode(textContent = '') {
|
||||
return { textContent };
|
||||
}
|
||||
|
||||
function createInputElement({
|
||||
id = '',
|
||||
type = 'text',
|
||||
placeholder = '',
|
||||
label = '',
|
||||
value = '',
|
||||
} = {}) {
|
||||
const attributes = {
|
||||
id,
|
||||
type,
|
||||
placeholder,
|
||||
autocomplete: 'off',
|
||||
};
|
||||
return {
|
||||
tagName: 'INPUT',
|
||||
id,
|
||||
type,
|
||||
value,
|
||||
labels: label ? [createTextNode(label)] : [],
|
||||
disabled: false,
|
||||
readOnly: false,
|
||||
form: null,
|
||||
getAttribute(name) {
|
||||
return attributes[name] || '';
|
||||
},
|
||||
getBoundingClientRect() {
|
||||
return { width: 455, height: 32 };
|
||||
},
|
||||
closest(selector) {
|
||||
if (selector === 'label') {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createButtonElement({ text = 'Continue', testId = 'test-primary-button' } = {}) {
|
||||
return {
|
||||
tagName: 'BUTTON',
|
||||
textContent: text,
|
||||
value: '',
|
||||
disabled: false,
|
||||
form: null,
|
||||
getAttribute(name) {
|
||||
if (name === 'data-testid') return testId;
|
||||
if (name === 'type') return 'submit';
|
||||
return '';
|
||||
},
|
||||
getBoundingClientRect() {
|
||||
return { width: 455, height: 32 };
|
||||
},
|
||||
closest() {
|
||||
return null;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createHarness({ href, hostname, title = '', bodyText = '' }) {
|
||||
return createDomHarness({ href, hostname, title, bodyText });
|
||||
}
|
||||
|
||||
function createDomHarness({
|
||||
href,
|
||||
hostname,
|
||||
title = '',
|
||||
bodyText = '',
|
||||
inputs = [],
|
||||
buttons = [],
|
||||
} = {}) {
|
||||
const elementsById = new Map();
|
||||
for (const element of [...inputs, ...buttons]) {
|
||||
if (element.id) {
|
||||
elementsById.set(element.id, element);
|
||||
}
|
||||
}
|
||||
const context = {
|
||||
console: { log() {}, warn() {}, error() {}, info() {} },
|
||||
location: { href, hostname },
|
||||
@@ -20,10 +99,22 @@ function createHarness({ href, hostname, title = '', bodyText = '' }) {
|
||||
},
|
||||
setAttribute() {},
|
||||
},
|
||||
getElementById(id) {
|
||||
return elementsById.get(id) || null;
|
||||
},
|
||||
querySelector() {
|
||||
return null;
|
||||
},
|
||||
querySelectorAll() {
|
||||
querySelectorAll(selector) {
|
||||
if (String(selector).startsWith('label[for=')) {
|
||||
return [];
|
||||
}
|
||||
if (String(selector).trim().startsWith('input')) {
|
||||
return inputs;
|
||||
}
|
||||
if (String(selector).includes('button') || String(selector).includes('[role="button"]')) {
|
||||
return buttons;
|
||||
}
|
||||
return [];
|
||||
},
|
||||
},
|
||||
@@ -37,14 +128,18 @@ function createHarness({ href, hostname, title = '', bodyText = '' }) {
|
||||
sleep() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
fillInput() {},
|
||||
fillInput(element, value) {
|
||||
element.value = value;
|
||||
},
|
||||
MouseEvent: class {},
|
||||
PointerEvent: class {},
|
||||
KeyboardEvent: class {},
|
||||
Event: class {},
|
||||
};
|
||||
context.window = context;
|
||||
context.globalThis = context;
|
||||
context.window.getComputedStyle = () => ({ display: 'block', visibility: 'visible' });
|
||||
context.CSS = { escape: (value) => String(value) };
|
||||
|
||||
vm.createContext(context);
|
||||
vm.runInContext(source, context);
|
||||
@@ -105,3 +200,48 @@ test('kiro register content extracts signed-in account email from Kiro account p
|
||||
assert.equal(detected.accountEmail, 'scrap-aged-quirk@duck.com');
|
||||
assert.equal(detected.email, 'scrap-aged-quirk@duck.com');
|
||||
});
|
||||
|
||||
test('kiro register content fills primary and confirm password fields separately', async () => {
|
||||
const passwordInput = createInputElement({
|
||||
id: 'formField15-1779204320095-7559',
|
||||
type: 'text',
|
||||
placeholder: 'Enter password',
|
||||
label: '\u5bc6\u7801',
|
||||
});
|
||||
const confirmPasswordInput = createInputElement({
|
||||
id: 'formField16-1779204320096-1309',
|
||||
type: 'text',
|
||||
placeholder: 'Re-enter password',
|
||||
label: '\u786e\u8ba4\u5bc6\u7801',
|
||||
});
|
||||
const checkboxInput = createInputElement({
|
||||
id: '17-1779204320097-4334',
|
||||
type: 'checkbox',
|
||||
label: '\u663e\u793a\u5bc6\u7801',
|
||||
value: 'on',
|
||||
});
|
||||
const continueButton = createButtonElement();
|
||||
const clicks = [];
|
||||
|
||||
const harness = createDomHarness({
|
||||
href: 'https://us-east-1.signin.aws/platform/d-9067642ac7/signup',
|
||||
hostname: 'us-east-1.signin.aws',
|
||||
title: 'Amazon Web Services',
|
||||
bodyText: 'Create your password',
|
||||
inputs: [passwordInput, confirmPasswordInput, checkboxInput],
|
||||
buttons: [continueButton],
|
||||
});
|
||||
harness.simulateClick = (element) => clicks.push(element);
|
||||
|
||||
const detected = harness.detectKiroRegisterPageState();
|
||||
assert.equal(detected.state, 'password_page');
|
||||
assert.equal(detected.passwordInput, passwordInput);
|
||||
assert.equal(detected.confirmPasswordInput, confirmPasswordInput);
|
||||
|
||||
const result = await harness.submitKiroPassword({ password: 'mdy8U9_rzqhw6D' });
|
||||
|
||||
assert.equal(result.state, 'password_submitted');
|
||||
assert.equal(passwordInput.value, 'mdy8U9_rzqhw6D');
|
||||
assert.equal(confirmPasswordInput.value, 'mdy8U9_rzqhw6D');
|
||||
assert.deepEqual(clicks, [continueButton]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user