Files
FlowPilot/tests/paypal-flow-content.test.js
T
2026-04-28 15:31:59 +08:00

311 lines
8.0 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('content/paypal-flow.js', 'utf8');
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
.map((marker) => source.indexOf(marker))
.find((index) => index >= 0);
if (start < 0) {
throw new Error(`missing function ${name}`);
}
let parenDepth = 0;
let signatureEnded = false;
let braceStart = -1;
for (let index = start; index < source.length; index += 1) {
const char = source[index];
if (char === '(') {
parenDepth += 1;
} else if (char === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (char === '{' && signatureEnded) {
braceStart = index;
break;
}
}
if (braceStart < 0) {
throw new Error(`missing body for function ${name}`);
}
let depth = 0;
let end = braceStart;
for (; end < source.length; end += 1) {
const char = source[end];
if (char === '{') depth += 1;
if (char === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return source.slice(start, end);
}
function createElement({
tag = 'div',
type = '',
id = '',
name = '',
text = '',
value = '',
placeholder = '',
attrs = {},
style = {},
rect = { width: 160, height: 40 },
parentElement = null,
} = {}) {
return {
nodeType: 1,
tag,
type,
id,
name,
textContent: text,
value,
placeholder,
disabled: false,
hidden: Boolean(attrs.hidden),
style: {
display: 'block',
visibility: 'visible',
opacity: '1',
...style,
},
parentElement,
getAttribute(key) {
if (key === 'type') return type;
if (key === 'id') return id;
if (key === 'name') return name;
if (key === 'placeholder') return placeholder;
if (key === 'value') return value;
return Object.prototype.hasOwnProperty.call(attrs, key) ? attrs[key] : null;
},
getBoundingClientRect() {
return rect;
},
};
}
function loadApi(elements) {
const document = {
documentElement: {},
querySelectorAll(selector) {
if (selector === 'input') {
return elements.filter((el) => el.tag === 'input');
}
if (selector === 'input[type="email"]') {
return elements.filter((el) => el.tag === 'input' && el.type === 'email');
}
if (selector === 'input[type="password"]') {
return elements.filter((el) => el.tag === 'input' && el.type === 'password');
}
if (selector.includes('button') || selector.includes('[role="button"]')) {
return elements.filter((el) => el.tag === 'button' || el.attrs?.role === 'button');
}
return [];
},
};
const window = {
getComputedStyle(el) {
return el?.style || { display: 'block', visibility: 'visible', opacity: '1' };
},
};
return new Function('document', 'window', `
${extractFunction('isVisibleElement')}
${extractFunction('normalizeText')}
${extractFunction('getActionText')}
${extractFunction('getVisibleControls')}
${extractFunction('isEnabledControl')}
${extractFunction('findClickableByText')}
${extractFunction('findInputByPatterns')}
${extractFunction('findEmailInput')}
${extractFunction('findPasswordInput')}
${extractFunction('findLoginNextButton')}
${extractFunction('findEmailNextButton')}
${extractFunction('findPasswordLoginButton')}
${extractFunction('getPayPalLoginPhase')}
return {
findEmailInput,
findPasswordInput,
findEmailNextButton,
findPasswordLoginButton,
getPayPalLoginPhase,
};
`)(document, window);
}
function createSubmitApi(overrides = {}) {
const bindings = {
waitForDocumentComplete: async () => {},
normalizeText: (text = '') => String(text || '').replace(/\s+/g, ' ').trim(),
findPasswordInput: () => null,
findEmailInput: () => null,
findEmailNextButton: () => null,
isEnabledControl: () => true,
findPasswordLoginButton: () => null,
fillInput: () => {},
simulateClick: () => {},
waitUntil: async (predicate) => predicate(),
findLoginNextButton: () => null,
sleep: async () => {},
...overrides,
};
return new Function(
'waitForDocumentComplete',
'normalizeText',
'findPasswordInput',
'findEmailInput',
'findEmailNextButton',
'isEnabledControl',
'findPasswordLoginButton',
'fillInput',
'simulateClick',
'waitUntil',
'findLoginNextButton',
'sleep',
`
${extractFunction('refillPayPalEmailInput')}
${extractFunction('submitPayPalLogin')}
return { refillPayPalEmailInput, submitPayPalLogin };
`
)(
bindings.waitForDocumentComplete,
bindings.normalizeText,
bindings.findPasswordInput,
bindings.findEmailInput,
bindings.findEmailNextButton,
bindings.isEnabledControl,
bindings.findPasswordLoginButton,
bindings.fillInput,
bindings.simulateClick,
bindings.waitUntil,
bindings.findLoginNextButton,
bindings.sleep
);
}
test('PayPal email page ignores hidden pre-rendered password input', () => {
const hiddenPanel = createElement({ attrs: { 'aria-hidden': 'true' } });
const emailInput = createElement({
tag: 'input',
type: 'text',
id: 'login_email',
name: 'login_email',
value: 'user@example.com',
placeholder: 'Email',
});
const hiddenPasswordInput = createElement({
tag: 'input',
type: 'password',
id: 'login_password',
name: 'login_password',
parentElement: hiddenPanel,
});
const nextButton = createElement({
tag: 'button',
id: 'btnNext',
text: 'Next',
});
const api = loadApi([emailInput, hiddenPasswordInput, nextButton]);
assert.equal(api.findEmailInput(), emailInput);
assert.equal(api.findPasswordInput(), null);
assert.equal(api.findEmailNextButton(), nextButton);
assert.equal(api.findPasswordLoginButton(), null);
assert.equal(api.getPayPalLoginPhase(emailInput, api.findPasswordInput()), 'email');
});
test('PayPal combined login page still sees visible password input', () => {
const emailInput = createElement({
tag: 'input',
type: 'text',
id: 'login_email',
name: 'login_email',
});
const passwordInput = createElement({
tag: 'input',
type: 'password',
id: 'login_password',
name: 'login_password',
});
const loginButton = createElement({
tag: 'button',
id: 'btnLogin',
text: 'Log In',
});
const api = loadApi([emailInput, passwordInput, loginButton]);
assert.equal(api.findEmailInput(), emailInput);
assert.equal(api.findPasswordInput(), passwordInput);
assert.equal(api.findPasswordLoginButton(), loginButton);
assert.equal(api.getPayPalLoginPhase(emailInput, passwordInput), 'login_combined');
});
test('PayPal email submit refills a prefilled email before clicking next', async () => {
const emailInput = createElement({
tag: 'input',
type: 'text',
id: 'login_email',
name: 'login_email',
value: 'user@example.com',
placeholder: 'Email',
});
const nextButton = createElement({
tag: 'button',
id: 'btnNext',
text: 'Next',
});
const fillValues = [];
const clicked = [];
let focusCount = 0;
let blurCount = 0;
emailInput.focus = () => {
focusCount += 1;
};
emailInput.blur = () => {
blurCount += 1;
};
const api = createSubmitApi({
findEmailInput: () => emailInput,
findEmailNextButton: () => nextButton,
fillInput: (element, value) => {
fillValues.push(value);
element.value = value;
},
simulateClick: (element) => {
clicked.push(element);
},
});
const result = await api.submitPayPalLogin({
email: 'user@example.com',
password: 'secret',
});
assert.deepEqual(fillValues, ['', 'user@example.com']);
assert.equal(focusCount, 1);
assert.equal(blurCount, 1);
assert.deepEqual(clicked, [nextButton]);
assert.deepEqual(result, {
submitted: false,
phase: 'email_submitted',
awaiting: 'password_page',
});
});