feat: add Japanese auth flow localization
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const source = fs.readFileSync('content/signup-page.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 i = start; i < source.length; i += 1) {
|
||||
const ch = source[i];
|
||||
if (ch === '(') {
|
||||
parenDepth += 1;
|
||||
} else if (ch === ')') {
|
||||
parenDepth -= 1;
|
||||
if (parenDepth === 0) {
|
||||
signatureEnded = true;
|
||||
}
|
||||
} else if (ch === '{' && signatureEnded) {
|
||||
braceStart = i;
|
||||
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 ch = source[end];
|
||||
if (ch === '{') depth += 1;
|
||||
if (ch === '}') {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
end += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return source.slice(start, end);
|
||||
}
|
||||
|
||||
function extractConst(name) {
|
||||
const pattern = new RegExp(`const\\s+${name}\\s*=\\s*[\\s\\S]*?;`);
|
||||
const match = source.match(pattern);
|
||||
if (!match) {
|
||||
throw new Error(`missing const ${name}`);
|
||||
}
|
||||
return match[0];
|
||||
}
|
||||
|
||||
test('login step recognizes Japanese email and phone entry actions', () => {
|
||||
const api = new Function(`
|
||||
const emailButton = {
|
||||
textContent: 'メールアドレスで続行',
|
||||
disabled: false,
|
||||
getAttribute(name) {
|
||||
if (name === 'aria-disabled') return 'false';
|
||||
return '';
|
||||
},
|
||||
};
|
||||
const phoneButton = {
|
||||
textContent: '電話番号で続行',
|
||||
disabled: false,
|
||||
getAttribute(name) {
|
||||
if (name === 'aria-disabled') return 'false';
|
||||
return '';
|
||||
},
|
||||
};
|
||||
const codeButton = {
|
||||
textContent: 'コードでサインイン',
|
||||
disabled: false,
|
||||
getAttribute(name) {
|
||||
if (name === 'aria-disabled') return 'false';
|
||||
return '';
|
||||
},
|
||||
};
|
||||
const document = {
|
||||
querySelectorAll(selector) {
|
||||
if (selector === 'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]') {
|
||||
return [codeButton, phoneButton, emailButton];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
};
|
||||
|
||||
${extractConst('ONE_TIME_CODE_LOGIN_PATTERN')}
|
||||
${extractConst('LOGIN_ENTRY_ACTION_PATTERN')}
|
||||
${extractConst('LOGIN_SWITCH_TO_PHONE_PATTERN')}
|
||||
${extractConst('LOGIN_PHONE_ACTION_PATTERN')}
|
||||
${extractConst('LOGIN_EXTERNAL_IDP_PATTERN')}
|
||||
${extractConst('LOGIN_CODE_ONLY_ACTION_PATTERN')}
|
||||
|
||||
function isVisibleElement(el) { return Boolean(el); }
|
||||
function isActionEnabled(el) { return Boolean(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true'; }
|
||||
function getActionText(el) { return [el?.textContent, el?.value, el?.getAttribute?.('aria-label'), el?.getAttribute?.('title')].filter(Boolean).join(' ').replace(/\\s+/g, ' ').trim(); }
|
||||
|
||||
${extractFunction('findLoginEntryTrigger')}
|
||||
${extractFunction('findLoginPhoneEntryTrigger')}
|
||||
${extractFunction('findOneTimeCodeLoginTrigger')}
|
||||
|
||||
return {
|
||||
email() { return getActionText(findLoginEntryTrigger()); },
|
||||
phone() { return getActionText(findLoginPhoneEntryTrigger()); },
|
||||
code() { return getActionText(findOneTimeCodeLoginTrigger()); },
|
||||
};
|
||||
`)();
|
||||
|
||||
assert.equal(api.email(), 'メールアドレスで続行');
|
||||
assert.equal(api.phone(), '電話番号で続行');
|
||||
assert.equal(api.code(), 'コードでサインイン');
|
||||
});
|
||||
|
||||
test('login submit button recognizes Japanese login and continue labels', () => {
|
||||
const api = new Function(`
|
||||
const loginButton = {
|
||||
textContent: 'ログイン',
|
||||
disabled: false,
|
||||
getAttribute(name) {
|
||||
if (name === 'type') return 'button';
|
||||
if (name === 'aria-disabled') return 'false';
|
||||
return '';
|
||||
},
|
||||
};
|
||||
const document = {
|
||||
querySelector() { return null; },
|
||||
querySelectorAll(selector) {
|
||||
if (selector === 'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]') {
|
||||
return [loginButton];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
};
|
||||
|
||||
${extractConst('ONE_TIME_CODE_LOGIN_PATTERN')}
|
||||
|
||||
function isVisibleElement(el) { return Boolean(el); }
|
||||
function isActionEnabled(el) { return Boolean(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true'; }
|
||||
function getActionText(el) { return [el?.textContent, el?.value, el?.getAttribute?.('aria-label'), el?.getAttribute?.('title')].filter(Boolean).join(' ').replace(/\\s+/g, ' ').trim(); }
|
||||
|
||||
${extractFunction('getLoginSubmitButton')}
|
||||
|
||||
return {
|
||||
run() { return getActionText(getLoginSubmitButton()); },
|
||||
};
|
||||
`)();
|
||||
|
||||
assert.equal(api.run(), 'ログイン');
|
||||
});
|
||||
|
||||
test('verification helpers recognize Japanese resend and submit actions', () => {
|
||||
const api = new Function(`
|
||||
const resendButton = {
|
||||
textContent: 'コードを再送信',
|
||||
disabled: false,
|
||||
getAttribute(name) {
|
||||
if (name === 'aria-disabled') return 'false';
|
||||
return '';
|
||||
},
|
||||
};
|
||||
const verifyButton = {
|
||||
textContent: '確認',
|
||||
disabled: false,
|
||||
getAttribute(name) {
|
||||
if (name === 'aria-disabled') return 'false';
|
||||
return '';
|
||||
},
|
||||
};
|
||||
const codeInput = {
|
||||
form: {
|
||||
querySelectorAll(selector) {
|
||||
if (selector === 'button[type="submit"], input[type="submit"]') return [];
|
||||
if (selector === 'button, [role="button"], input[type="button"], input[type="submit"]') return [verifyButton];
|
||||
return [];
|
||||
},
|
||||
},
|
||||
closest() { return null; },
|
||||
};
|
||||
const document = {
|
||||
querySelectorAll(selector) {
|
||||
if (selector === 'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]') {
|
||||
return [resendButton];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
};
|
||||
|
||||
${extractConst('RESEND_VERIFICATION_CODE_PATTERN')}
|
||||
|
||||
function isVisibleElement(el) { return Boolean(el); }
|
||||
function isActionEnabled(el) { return Boolean(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true'; }
|
||||
function getActionText(el) { return [el?.textContent, el?.value, el?.getAttribute?.('aria-label'), el?.getAttribute?.('title')].filter(Boolean).join(' ').replace(/\\s+/g, ' ').trim(); }
|
||||
|
||||
${extractFunction('findResendVerificationCodeTrigger')}
|
||||
${extractFunction('getVerificationSubmitButtonForTarget')}
|
||||
|
||||
return {
|
||||
resend() { return getActionText(findResendVerificationCodeTrigger()); },
|
||||
submit() { return getActionText(getVerificationSubmitButtonForTarget(codeInput)); },
|
||||
};
|
||||
`)();
|
||||
|
||||
assert.equal(api.resend(), 'コードを再送信');
|
||||
assert.equal(api.submit(), '確認');
|
||||
});
|
||||
|
||||
test('OAuth consent detection recognizes Japanese consent copy and continue button', () => {
|
||||
const api = new Function(`
|
||||
const continueButton = {
|
||||
textContent: '続行',
|
||||
disabled: false,
|
||||
getAttribute(name) {
|
||||
if (name === 'data-dd-action-name') return '';
|
||||
if (name === 'aria-disabled') return 'false';
|
||||
return '';
|
||||
},
|
||||
};
|
||||
const consentForm = {
|
||||
querySelectorAll(selector) {
|
||||
if (selector === 'button[type="submit"], input[type="submit"], [role="button"]') {
|
||||
return [continueButton];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
};
|
||||
const document = {
|
||||
body: {
|
||||
innerText: 'ChatGPTを使用してCodexにサインイン',
|
||||
textContent: 'ChatGPTを使用してCodexにサインイン',
|
||||
},
|
||||
querySelector(selector) {
|
||||
if (selector === OAUTH_CONSENT_FORM_SELECTOR) return consentForm;
|
||||
return null;
|
||||
},
|
||||
querySelectorAll(selector) {
|
||||
if (selector === 'button, [role="button"]') return [continueButton];
|
||||
return [];
|
||||
},
|
||||
};
|
||||
|
||||
${extractConst('OAUTH_CONSENT_PAGE_PATTERN')}
|
||||
${extractConst('OAUTH_CONSENT_FORM_SELECTOR')}
|
||||
${extractConst('CONTINUE_ACTION_PATTERN')}
|
||||
|
||||
function isVisibleElement(el) { return Boolean(el); }
|
||||
function getActionText(el) { return [el?.textContent, el?.value, el?.getAttribute?.('aria-label'), el?.getAttribute?.('title')].filter(Boolean).join(' ').replace(/\\s+/g, ' ').trim(); }
|
||||
|
||||
${extractFunction('getPageTextSnapshot')}
|
||||
${extractFunction('getOAuthConsentForm')}
|
||||
${extractFunction('getPrimaryContinueButton')}
|
||||
${extractFunction('isOAuthConsentPage')}
|
||||
|
||||
return {
|
||||
consent() { return isOAuthConsentPage(); },
|
||||
button() { return getActionText(getPrimaryContinueButton()); },
|
||||
};
|
||||
`)();
|
||||
|
||||
assert.equal(api.consent(), true);
|
||||
assert.equal(api.button(), '続行');
|
||||
});
|
||||
|
||||
test('profile step recognizes Japanese account creation and day label', () => {
|
||||
const api = new Function(`
|
||||
const submitButton = {
|
||||
textContent: 'アカウントを作成',
|
||||
getAttribute(name) {
|
||||
if (name === 'type') return 'button';
|
||||
return '';
|
||||
},
|
||||
};
|
||||
const dayLabel = { textContent: '日' };
|
||||
const dayRoot = {
|
||||
querySelectorAll(selector) {
|
||||
if (selector === 'span') return [dayLabel];
|
||||
return [];
|
||||
},
|
||||
querySelector(selector) {
|
||||
if (selector === 'button[aria-haspopup="listbox"]') return { id: 'day-button' };
|
||||
if (selector === '.react-aria-SelectValue') return { textContent: '' };
|
||||
return null;
|
||||
},
|
||||
closest() { return null; },
|
||||
parentElement: {
|
||||
querySelector(selector) {
|
||||
if (selector === '[data-testid="hidden-select-container"] select') return { id: 'day-select' };
|
||||
return null;
|
||||
},
|
||||
},
|
||||
};
|
||||
const document = {
|
||||
querySelector(selector) {
|
||||
if (selector === 'button[type="submit"], input[type="submit"]') return null;
|
||||
return null;
|
||||
},
|
||||
querySelectorAll(selector) {
|
||||
if (selector === 'button, [role="button"], input[type="button"], input[type="submit"]') return [submitButton];
|
||||
if (selector === '.react-aria-Select') return [dayRoot];
|
||||
return [];
|
||||
},
|
||||
};
|
||||
|
||||
function isVisibleElement(el) { return Boolean(el); }
|
||||
function getActionText(el) { return [el?.textContent, el?.value, el?.getAttribute?.('aria-label'), el?.getAttribute?.('title')].filter(Boolean).join(' ').replace(/\\s+/g, ' ').trim(); }
|
||||
|
||||
${extractFunction('normalizeInlineText')}
|
||||
${extractFunction('getStep5SubmitButton')}
|
||||
${extractFunction('findBirthdayReactAriaSelect')}
|
||||
|
||||
return {
|
||||
submit() { return getActionText(getStep5SubmitButton()); },
|
||||
day() { return findBirthdayReactAriaSelect('日')?.nativeSelect?.id || ''; },
|
||||
};
|
||||
`)();
|
||||
|
||||
assert.equal(api.submit(), 'アカウントを作成');
|
||||
assert.equal(api.day(), 'day-select');
|
||||
});
|
||||
@@ -440,6 +440,128 @@ return {
|
||||
assert.equal(api.run()?.kind, 'localized-email');
|
||||
});
|
||||
|
||||
test('findSignupEntryTrigger recognizes Japanese free signup button text', () => {
|
||||
const api = new Function(`
|
||||
const signupButton = {
|
||||
textContent: '無料でサインアップ',
|
||||
value: '',
|
||||
disabled: false,
|
||||
getAttribute(name) {
|
||||
if (name === 'type') return 'button';
|
||||
return '';
|
||||
},
|
||||
getBoundingClientRect() {
|
||||
return { width: 180, height: 48 };
|
||||
},
|
||||
};
|
||||
|
||||
const loginButton = {
|
||||
textContent: 'ログイン',
|
||||
value: '',
|
||||
disabled: false,
|
||||
getAttribute(name) {
|
||||
if (name === 'type') return 'button';
|
||||
return '';
|
||||
},
|
||||
getBoundingClientRect() {
|
||||
return { width: 120, height: 48 };
|
||||
},
|
||||
};
|
||||
|
||||
const document = {
|
||||
querySelectorAll(selector) {
|
||||
if (selector === 'a, button, [role="button"], [role="link"]') {
|
||||
return [loginButton, signupButton];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
};
|
||||
|
||||
${extractConst('SIGNUP_ENTRY_TRIGGER_PATTERN')}
|
||||
|
||||
function isVisibleElement(el) {
|
||||
return Boolean(el);
|
||||
}
|
||||
|
||||
function isActionEnabled(el) {
|
||||
return Boolean(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true';
|
||||
}
|
||||
|
||||
function getActionText(el) {
|
||||
return [el?.textContent, el?.value, el?.getAttribute?.('aria-label'), el?.getAttribute?.('title')]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.replace(/\\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
${extractFunction('findSignupEntryTrigger')}
|
||||
|
||||
return {
|
||||
run() {
|
||||
return getActionText(findSignupEntryTrigger());
|
||||
},
|
||||
};
|
||||
`)();
|
||||
|
||||
assert.equal(api.run(), '無料でサインアップ');
|
||||
});
|
||||
|
||||
test('getSignupEmailContinueButton recognizes Japanese continue text', () => {
|
||||
const api = new Function(`
|
||||
const continueButton = {
|
||||
textContent: '続ける',
|
||||
value: '',
|
||||
disabled: false,
|
||||
getAttribute(name) {
|
||||
if (name === 'type') return 'button';
|
||||
return '';
|
||||
},
|
||||
getBoundingClientRect() {
|
||||
return { width: 180, height: 48 };
|
||||
},
|
||||
};
|
||||
|
||||
const document = {
|
||||
querySelector() {
|
||||
return null;
|
||||
},
|
||||
querySelectorAll(selector) {
|
||||
if (selector === 'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]') {
|
||||
return [continueButton];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
};
|
||||
|
||||
function isVisibleElement(el) {
|
||||
return Boolean(el);
|
||||
}
|
||||
|
||||
function isActionEnabled(el) {
|
||||
return Boolean(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true';
|
||||
}
|
||||
|
||||
function getActionText(el) {
|
||||
return [el?.textContent, el?.value, el?.getAttribute?.('aria-label'), el?.getAttribute?.('title')]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.replace(/\\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
${extractFunction('getSignupEmailContinueButton')}
|
||||
|
||||
return {
|
||||
run() {
|
||||
return getActionText(getSignupEmailContinueButton());
|
||||
},
|
||||
};
|
||||
`)();
|
||||
|
||||
assert.equal(api.run(), '続ける');
|
||||
});
|
||||
|
||||
test('waitForSignupEntryState retries the signup entry click five times before giving up', async () => {
|
||||
const api = new Function(`
|
||||
const logs = [];
|
||||
|
||||
Reference in New Issue
Block a user