diff --git a/content/kiro/register-page.js b/content/kiro/register-page.js index 9716c8e..2f94083 100644 --- a/content/kiro/register-page.js +++ b/content/kiro/register-page.js @@ -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); diff --git a/tests/kiro-register-page-fatal-state.test.js b/tests/kiro-register-page-fatal-state.test.js index 95e6a94..475136b 100644 --- a/tests/kiro-register-page-fatal-state.test.js +++ b/tests/kiro-register-page-fatal-state.test.js @@ -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]); +});