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]',
|
'input[name*="code" i]',
|
||||||
].join(', ');
|
].join(', ');
|
||||||
|
|
||||||
const KIRO_PASSWORD_INPUT_SELECTOR = [
|
const KIRO_PASSWORD_FIELD_SELECTOR = 'input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]):not([type="submit"]):not([type="button"]):not([type="file"])';
|
||||||
'input[placeholder="Enter password"]',
|
const KIRO_PASSWORD_TEXT_PATTERN = /password|\u5bc6\u7801/i;
|
||||||
'input[placeholder*="Create password" 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;
|
||||||
'input[placeholder*="Password" 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;
|
||||||
'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(', ');
|
|
||||||
|
|
||||||
function isVisibleKiroElement(element) {
|
function isVisibleKiroElement(element) {
|
||||||
if (!element) return false;
|
if (!element) return false;
|
||||||
@@ -190,12 +178,156 @@ function findVisibleOtpInput() {
|
|||||||
return findFirstVisible(KIRO_OTP_INPUT_SELECTOR);
|
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() {
|
function findVisiblePasswordInputs() {
|
||||||
return collectVisibleElements(KIRO_PASSWORD_INPUT_SELECTOR);
|
return findKiroPasswordFields().passwordInputs;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findVisibleConfirmPasswordInput() {
|
function findVisibleConfirmPasswordInput() {
|
||||||
return findFirstVisible(KIRO_CONFIRM_PASSWORD_SELECTOR);
|
return findKiroPasswordFields().confirmPasswordInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findEmailContinueButton(emailInput = null) {
|
function findEmailContinueButton(emailInput = null) {
|
||||||
@@ -378,15 +510,14 @@ function detectKiroRegisterPageState() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const passwordInputs = findVisiblePasswordInputs();
|
const passwordFields = findKiroPasswordFields();
|
||||||
const confirmPasswordInput = findVisibleConfirmPasswordInput();
|
if (passwordFields.passwordInput) {
|
||||||
if (passwordInputs.length) {
|
const passwordInput = passwordFields.passwordInput;
|
||||||
const passwordInput = passwordInputs[0];
|
|
||||||
return {
|
return {
|
||||||
state: 'password_page',
|
state: 'password_page',
|
||||||
url: currentUrl,
|
url: currentUrl,
|
||||||
passwordInput,
|
passwordInput,
|
||||||
confirmPasswordInput,
|
confirmPasswordInput: passwordFields.confirmPasswordInput,
|
||||||
continueButton: findPasswordContinueButton(passwordInput),
|
continueButton: findPasswordContinueButton(passwordInput),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -645,15 +776,29 @@ async function submitKiroPassword(payload = {}) {
|
|||||||
|
|
||||||
fillInput(readyState.passwordInput, password);
|
fillInput(readyState.passwordInput, password);
|
||||||
await sleep(150);
|
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 confirmPasswordInput = readyState.confirmPasswordInput
|
||||||
|| (() => {
|
|| (() => {
|
||||||
const passwordInputs = findVisiblePasswordInputs();
|
const passwordInputs = findVisiblePasswordInputs();
|
||||||
return passwordInputs.length > 1 ? passwordInputs[1] : null;
|
return passwordInputs.find((input) => input !== readyState.passwordInput) || null;
|
||||||
})();
|
})();
|
||||||
if (confirmPasswordInput && confirmPasswordInput !== readyState.passwordInput) {
|
if (confirmPasswordInput && confirmPasswordInput !== readyState.passwordInput) {
|
||||||
fillInput(confirmPasswordInput, password);
|
fillInput(confirmPasswordInput, password);
|
||||||
await sleep(150);
|
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);
|
simulateClick(readyState.continueButton);
|
||||||
|
|||||||
@@ -5,7 +5,86 @@ const vm = require('node:vm');
|
|||||||
|
|
||||||
const source = fs.readFileSync('content/kiro/register-page.js', 'utf8');
|
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 = '' }) {
|
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 = {
|
const context = {
|
||||||
console: { log() {}, warn() {}, error() {}, info() {} },
|
console: { log() {}, warn() {}, error() {}, info() {} },
|
||||||
location: { href, hostname },
|
location: { href, hostname },
|
||||||
@@ -20,10 +99,22 @@ function createHarness({ href, hostname, title = '', bodyText = '' }) {
|
|||||||
},
|
},
|
||||||
setAttribute() {},
|
setAttribute() {},
|
||||||
},
|
},
|
||||||
|
getElementById(id) {
|
||||||
|
return elementsById.get(id) || null;
|
||||||
|
},
|
||||||
querySelector() {
|
querySelector() {
|
||||||
return null;
|
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 [];
|
return [];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -37,14 +128,18 @@ function createHarness({ href, hostname, title = '', bodyText = '' }) {
|
|||||||
sleep() {
|
sleep() {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
fillInput() {},
|
fillInput(element, value) {
|
||||||
|
element.value = value;
|
||||||
|
},
|
||||||
MouseEvent: class {},
|
MouseEvent: class {},
|
||||||
PointerEvent: class {},
|
PointerEvent: class {},
|
||||||
KeyboardEvent: class {},
|
KeyboardEvent: class {},
|
||||||
|
Event: class {},
|
||||||
};
|
};
|
||||||
context.window = context;
|
context.window = context;
|
||||||
context.globalThis = context;
|
context.globalThis = context;
|
||||||
context.window.getComputedStyle = () => ({ display: 'block', visibility: 'visible' });
|
context.window.getComputedStyle = () => ({ display: 'block', visibility: 'visible' });
|
||||||
|
context.CSS = { escape: (value) => String(value) };
|
||||||
|
|
||||||
vm.createContext(context);
|
vm.createContext(context);
|
||||||
vm.runInContext(source, 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.accountEmail, 'scrap-aged-quirk@duck.com');
|
||||||
assert.equal(detected.email, '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