Add Korean Japanese and English user interfaces
This commit is contained in:
@@ -70,6 +70,14 @@ try {
|
||||
`for(let i=0;i<60;i++){try{const r=await fetch('http://127.0.0.1:${port}/api/health');if(r.ok){console.log('ready');break}}catch{}if(i===59)throw Error('not ready');await new Promise(r=>setTimeout(r,250))}`,
|
||||
);
|
||||
wait();
|
||||
if (process.env.HOP_CHECK_LANGUAGES === '1') {
|
||||
assert.equal(
|
||||
node(
|
||||
`const base='http://127.0.0.1:${port}';for(const language of ['en','ja']){const headers={Origin:'https://hop.test','Content-Type':'application/json','Accept-Language':language};const created=await fetch(base+'/api/guest/session',{method:'POST',headers,body:'{}'});const session=await created.json();if(!created.ok||session.defaultLanguage!==language)throw Error('wrong initial language');const cookie=created.headers.get('set-cookie').split(';')[0];const invalid=await fetch(base+'/api/device/me',{method:'PATCH',headers:{...headers,Cookie:cookie,'X-CSRF-Token':session.csrfToken},body:JSON.stringify({name:''})});const body=await invalid.json();if(body.code!=='VALIDATION_ERROR'||/[가-힣]/.test(body.message))throw Error('untranslated system message');}const manifest=await(await fetch(base+'/manifest.ja.webmanifest')).json();if(manifest.lang!=='ja')throw Error('missing localized manifest');console.log('languages ready');`,
|
||||
),
|
||||
'languages ready',
|
||||
);
|
||||
}
|
||||
const state = JSON.parse(
|
||||
node(
|
||||
`const base='http://127.0.0.1:${port}';const r=await fetch(base+'/api/guest/session',{method:'POST',headers:{Origin:'https://hop.test','Content-Type':'application/json'},body:'{}'});if(!r.ok)throw Error('session failed');const session=await r.json(),cookie=r.headers.get('set-cookie').split(';')[0];if(!r.headers.get('set-cookie').includes('Secure'))throw Error('insecure cookie');const renamed=await fetch(base+'/api/device/me',{method:'PATCH',headers:{Origin:'https://hop.test',Cookie:cookie,'X-CSRF-Token':session.csrfToken,'Content-Type':'application/json'},body:JSON.stringify({name:'Docker 저장 검증'})});if(!renamed.ok)throw Error('rename failed');console.log(JSON.stringify({cookie,id:session.device.id}));`,
|
||||
|
||||
@@ -57,6 +57,7 @@ try {
|
||||
const errors = [];
|
||||
async function page(width = 1280, ip = '8.8.4.4', country = 'KR') {
|
||||
const context = await browser.newContext({
|
||||
locale: 'ko-KR',
|
||||
viewport: { width, height: 900 },
|
||||
isMobile: width < 600,
|
||||
hasTouch: width < 600,
|
||||
@@ -67,6 +68,7 @@ try {
|
||||
'CF-Region': country === 'CN' ? 'Beijing' : 'Daejeon',
|
||||
},
|
||||
});
|
||||
await context.addInitScript(() => localStorage.setItem('hop-language', 'ko'));
|
||||
const page = await context.newPage();
|
||||
page.on('pageerror', (error) => errors.push(error.message));
|
||||
await page.goto(url);
|
||||
|
||||
@@ -28,8 +28,8 @@ try {
|
||||
? { executablePath: process.env.HOP_BROWSER_EXECUTABLE }
|
||||
: {}),
|
||||
});
|
||||
const a = await browser.newContext(),
|
||||
b = await browser.newContext();
|
||||
const a = await browser.newContext({ locale: 'ko-KR' }),
|
||||
b = await browser.newContext({ locale: 'ko-KR' });
|
||||
const sender = await a.newPage(),
|
||||
receiver = await b.newPage(),
|
||||
errors = [];
|
||||
|
||||
@@ -32,10 +32,12 @@ try {
|
||||
: {}),
|
||||
});
|
||||
const a = await browser.newContext({
|
||||
locale: 'ko-KR',
|
||||
viewport: { width: 1280, height: 900 },
|
||||
permissions: ['clipboard-read', 'clipboard-write'],
|
||||
});
|
||||
const b = await browser.newContext({
|
||||
locale: 'ko-KR',
|
||||
viewport: { width: 390, height: 844 },
|
||||
permissions: ['clipboard-read', 'clipboard-write'],
|
||||
});
|
||||
|
||||
@@ -29,8 +29,12 @@ try {
|
||||
? { executablePath: process.env.HOP_BROWSER_EXECUTABLE }
|
||||
: {}),
|
||||
});
|
||||
const hostContext = await browser.newContext({ viewport: { width: 1440, height: 1050 } }),
|
||||
const hostContext = await browser.newContext({
|
||||
locale: 'ko-KR',
|
||||
viewport: { width: 1440, height: 1050 },
|
||||
}),
|
||||
guestContext = await browser.newContext({
|
||||
locale: 'ko-KR',
|
||||
viewport: { width: 390, height: 844 },
|
||||
isMobile: true,
|
||||
hasTouch: true,
|
||||
|
||||
@@ -31,7 +31,10 @@ try {
|
||||
? { executablePath: process.env.HOP_BROWSER_EXECUTABLE }
|
||||
: {}),
|
||||
});
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 1000 } });
|
||||
const context = await browser.newContext({
|
||||
locale: 'ko-KR',
|
||||
viewport: { width: 1280, height: 1000 },
|
||||
});
|
||||
const page = await context.newPage(),
|
||||
errors = [];
|
||||
page.on('pageerror', (error) => errors.push(error.message));
|
||||
|
||||
302
tests/e2e/localization.mjs
Normal file
302
tests/e2e/localization.mjs
Normal file
@@ -0,0 +1,302 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { chromium, expect } from '@playwright/test';
|
||||
import { mkdtemp, rm, readFile } from 'node:fs/promises';
|
||||
import { resolve } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { createApp } from '../../apps/server/src/app.ts';
|
||||
import { loadConfig } from '../../apps/server/src/config.ts';
|
||||
const folder = await mkdtemp(resolve(tmpdir(), 'hop-languages-')),
|
||||
mails = [];
|
||||
const runtime = await createApp(
|
||||
{
|
||||
...loadConfig({
|
||||
APP_URL: 'http://127.0.0.1',
|
||||
DATABASE_PATH: resolve(folder, 'db.sqlite'),
|
||||
UPLOAD_DIR: resolve(folder, 'uploads'),
|
||||
MIN_FREE_DISK_MB: '0',
|
||||
LOCATION_DISCOVERY_ENABLED: 'false',
|
||||
CLIENT_IP_SOURCE: 'cloudflare',
|
||||
TRUSTED_PROXY_CIDRS: '127.0.0.1,::1',
|
||||
REGISTRATION_MODE: 'open',
|
||||
SESSION_SECRET: 's'.repeat(40),
|
||||
DISCOVERY_SECRET: 'd'.repeat(40),
|
||||
}),
|
||||
webRoot: resolve('apps/web/dist'),
|
||||
},
|
||||
{ mailer: async (mail) => mails.push(mail) },
|
||||
);
|
||||
let browser;
|
||||
try {
|
||||
await runtime.app.listen({ host: '127.0.0.1', port: 0 });
|
||||
const url = 'http://127.0.0.1:' + runtime.app.server.address().port;
|
||||
runtime.setDevOrigin(url);
|
||||
runtime.service.cfg.appUrl = url;
|
||||
browser = await chromium.launch({
|
||||
headless: true,
|
||||
...(process.env.HOP_BROWSER_EXECUTABLE
|
||||
? { executablePath: process.env.HOP_BROWSER_EXECUTABLE }
|
||||
: {}),
|
||||
});
|
||||
const errors = [];
|
||||
async function visitor(country, locale = 'en-US', preference, width = 390) {
|
||||
const context = await browser.newContext({
|
||||
locale,
|
||||
viewport: { width, height: 900 },
|
||||
isMobile: width < 600,
|
||||
hasTouch: width < 600,
|
||||
extraHTTPHeaders: {
|
||||
'CF-Connecting-IP': '8.8.8.8',
|
||||
...(country ? { 'CF-IPCountry': country } : {}),
|
||||
},
|
||||
});
|
||||
await context.addInitScript(
|
||||
({ preference }) => {
|
||||
if (preference) localStorage.setItem('hop-language', preference);
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
configurable: true,
|
||||
value: {
|
||||
readText: async () => '',
|
||||
writeText: async (text) => {
|
||||
window.__copied = text;
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
{ preference },
|
||||
);
|
||||
const page = await context.newPage();
|
||||
page.on('pageerror', (e) => errors.push(e.message));
|
||||
await page.goto(url);
|
||||
await page.locator('.app-shell[data-online="true"]').waitFor();
|
||||
await page.locator('.initial-device-loading').waitFor({ state: 'hidden' });
|
||||
return page;
|
||||
}
|
||||
async function language(page, value) {
|
||||
await page.locator('.language-picker select').selectOption(value);
|
||||
await expect(page.locator('html')).toHaveAttribute('lang', value);
|
||||
}
|
||||
async function noKoreanUI(page) {
|
||||
const text = await page.evaluate(() => {
|
||||
const clone = document.body.cloneNode(true);
|
||||
clone.querySelectorAll('.language-picker,script,style').forEach((el) => el.remove());
|
||||
return clone.textContent;
|
||||
});
|
||||
assert.equal(/[가-힣]/.test(text), false, text);
|
||||
assert.equal(
|
||||
await page.evaluate(() => document.documentElement.scrollWidth > innerWidth),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
await page.locator('.login-button').evaluate((button) => {
|
||||
const node = [...button.childNodes].find(
|
||||
(node) => node.nodeType === 3 && node.textContent.trim(),
|
||||
);
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(node);
|
||||
return range.getClientRects().length;
|
||||
}),
|
||||
1,
|
||||
'header login text stays on one line',
|
||||
);
|
||||
}
|
||||
for (const [country, browserLocale, chosen] of [
|
||||
['KR', 'en-US', 'ko'],
|
||||
['US', 'ja-JP', 'en'],
|
||||
[null, 'ja-JP', 'ja'],
|
||||
[null, 'fr-FR', 'en'],
|
||||
['JP', 'en-US', 'ja'],
|
||||
]) {
|
||||
const page = await visitor(country, browserLocale);
|
||||
await expect(page.locator('html')).toHaveAttribute('lang', chosen);
|
||||
if (chosen !== 'ko') await noKoreanUI(page);
|
||||
await page.context().close();
|
||||
}
|
||||
const explicit = await visitor('JP', 'ja-JP', 'en');
|
||||
await expect(explicit.locator('html')).toHaveAttribute('lang', 'en');
|
||||
await explicit.context().close();
|
||||
// A manual choice while the initial country response is in flight wins.
|
||||
const racing = await browser.newContext({
|
||||
locale: 'en-US',
|
||||
extraHTTPHeaders: { 'CF-Connecting-IP': '8.8.8.8', 'CF-IPCountry': 'KR' },
|
||||
});
|
||||
const race = await racing.newPage();
|
||||
let release;
|
||||
const gate = new Promise((resolve) => (release = resolve));
|
||||
await race.route('**/api/guest/session', async (route) => {
|
||||
await gate;
|
||||
await route.continue();
|
||||
});
|
||||
await race.goto(url);
|
||||
await language(race, 'ja');
|
||||
release();
|
||||
await race.locator('.app-shell[data-online="true"]').waitFor();
|
||||
await expect(race.locator('html')).toHaveAttribute('lang', 'ja');
|
||||
await racing.close();
|
||||
|
||||
const sender = await visitor('US', 'ja-JP', undefined, 1280),
|
||||
receiver = await visitor('JP', 'en-US');
|
||||
await expect(sender.locator('.login-button')).toHaveText('Log in');
|
||||
await expect(receiver.locator('.login-button')).toHaveText('ログイン');
|
||||
const deviceBefore = await receiver.evaluate(
|
||||
async () => (await (await fetch('/api/device/me')).json()).device.id,
|
||||
);
|
||||
await language(receiver, 'en');
|
||||
await receiver.reload();
|
||||
await expect(receiver.locator('html')).toHaveAttribute('lang', 'en');
|
||||
await receiver.locator('.device-name').waitFor();
|
||||
assert.equal(
|
||||
await receiver.evaluate(async () => (await (await fetch('/api/device/me')).json()).device.id),
|
||||
deviceBefore,
|
||||
);
|
||||
await language(receiver, 'ja');
|
||||
await expect(receiver.locator('link[rel="manifest"]')).toHaveAttribute(
|
||||
'href',
|
||||
'/manifest.ja.webmanifest',
|
||||
);
|
||||
assert.equal(
|
||||
(await receiver.evaluate(async () => await (await fetch('/manifest.ja.webmanifest')).json()))
|
||||
.lang,
|
||||
'ja',
|
||||
);
|
||||
|
||||
// Registration, mail and verification all use English; administrator content remains Korean.
|
||||
await sender.locator('.login-button').click();
|
||||
await sender.getByRole('button', { name: 'Sign up with email', exact: true }).click();
|
||||
await sender.setViewportSize({ width: 320, height: 900 });
|
||||
await noKoreanUI(sender);
|
||||
await sender.screenshot({ path: 'tests/e2e/i18n-register-en-mobile.png', fullPage: true });
|
||||
await sender.setViewportSize({ width: 1280, height: 900 });
|
||||
await sender.getByLabel('Email', { exact: true }).fill('hello@noname.company');
|
||||
await sender.getByLabel('Display name', { exact: true }).fill('Japan 日本語 한글');
|
||||
await sender.getByLabel('Password', { exact: true }).fill('12345678');
|
||||
await sender.getByLabel('Confirm password', { exact: true }).fill('12345678');
|
||||
await sender
|
||||
.getByRole('button', { name: 'Sign up and get verification email', exact: true })
|
||||
.click();
|
||||
await expect(sender.locator('.auth-message')).toContainText('verification email');
|
||||
assert.equal(mails.at(-1).subject, 'hop. Verify your email');
|
||||
const token = mails.at(-1).text.match(/#verify=([\w-]+)/)[1];
|
||||
await sender.goto(url + '/#verify=' + token);
|
||||
await sender.getByRole('button', { name: 'Verify email', exact: true }).click();
|
||||
await expect(sender.getByRole('tab', { name: 'Received', exact: true })).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true',
|
||||
);
|
||||
await sender.getByRole('button', { name: 'Account settings', exact: true }).click();
|
||||
const settings = sender.getByRole('dialog', { name: 'Account settings', exact: true });
|
||||
await settings.getByRole('button', { name: 'Change password', exact: true }).click();
|
||||
await expect(settings.getByLabel('New password', { exact: true })).toBeVisible();
|
||||
await settings.getByRole('button', { name: 'Close account settings', exact: true }).click();
|
||||
await sender.getByRole('button', { name: 'hop. Home', exact: true }).click();
|
||||
|
||||
// Create in English and join in Japanese.
|
||||
await sender.getByRole('button', { name: 'Connect another device', exact: true }).click();
|
||||
await sender.getByRole('button', { name: 'Create a room', exact: true }).click();
|
||||
await sender.getByRole('button', { name: 'Create', exact: true }).click();
|
||||
const code = (await sender.locator('.invite-code').innerText()).replace(/\s/g, '');
|
||||
await sender.getByRole('button', { name: 'Close dialog', exact: true }).click();
|
||||
await receiver.getByRole('button', { name: 'ほかのデバイスと接続', exact: true }).click();
|
||||
await receiver.getByRole('button', { name: 'ルームに参加', exact: true }).click();
|
||||
await receiver.getByRole('textbox', { name: '招待コード', exact: true }).fill(code);
|
||||
await receiver.getByRole('button', { name: '参加する', exact: true }).click();
|
||||
await receiver.locator('.room-leave').waitFor();
|
||||
await receiver.locator('.modal-backdrop').waitFor({ state: 'hidden' });
|
||||
|
||||
await sender.locator('.peer-card').first().click();
|
||||
const original = '로그인\n日本語 😀 <script>alert(1)</script> ';
|
||||
await sender.getByLabel('Text to send', { exact: true }).fill(original);
|
||||
// Another tab changes language without remounting or replacing the draft.
|
||||
const other = await sender.context().newPage();
|
||||
await other.goto(url);
|
||||
await other.locator('.device-name').waitFor();
|
||||
await language(other, 'ja');
|
||||
await expect(sender.getByLabel('送信するテキスト', { exact: true })).toHaveValue(original);
|
||||
await language(other, 'en');
|
||||
await other.close();
|
||||
await expect(sender.getByLabel('Text to send', { exact: true })).toHaveValue(original);
|
||||
await sender.getByRole('button', { name: 'Send', exact: true }).click();
|
||||
const request = receiver.getByRole('dialog', { name: '受信の承認リクエスト', exact: true });
|
||||
await expect(request.locator('.consent-kind')).toHaveText('テキストの受信');
|
||||
await receiver.screenshot({ path: 'tests/e2e/i18n-request-ja.png', fullPage: true });
|
||||
await request.getByRole('button', { name: '承認', exact: true }).click();
|
||||
const received = receiver.getByRole('dialog', { name: '受信したテキスト', exact: true });
|
||||
await expect(received.getByLabel('受信したテキストの内容', { exact: true })).toHaveText(original);
|
||||
await received.getByRole('button', { name: 'クリップボードにコピー', exact: true }).click();
|
||||
assert.equal(await receiver.evaluate(() => window.__copied), original);
|
||||
await receiver.screenshot({ path: 'tests/e2e/i18n-received-ja.png', fullPage: true });
|
||||
await received.getByRole('button', { name: '受信したテキストを閉じる', exact: true }).click();
|
||||
await sender.locator('.send-dialog').waitFor({ state: 'hidden' });
|
||||
|
||||
await sender.locator('.peer-card').first().click();
|
||||
await sender.getByLabel('Text to send', { exact: true }).fill('https://example.com/rejected');
|
||||
await sender.getByRole('button', { name: 'Send', exact: true }).click();
|
||||
await expect(request.locator('.consent-kind')).toHaveText('リンクの受信');
|
||||
await request.getByRole('button', { name: '辞退', exact: true }).click();
|
||||
await sender.locator('.send-dialog').waitFor({ state: 'hidden' });
|
||||
await expect(sender.locator('.toast')).toContainText('declined the transfer');
|
||||
|
||||
await sender.locator('.peer-card').first().click();
|
||||
const filename = '파일 日本語.txt',
|
||||
bytes = Buffer.from('원문 그대로 日本語 😀');
|
||||
await sender
|
||||
.getByLabel('Choose files to send', { exact: true })
|
||||
.setInputFiles({ name: filename, mimeType: 'text/plain', buffer: bytes });
|
||||
await sender.getByRole('button', { name: 'Send', exact: true }).click();
|
||||
await expect(request.locator('.consent-kind')).toHaveText('ファイルの受信');
|
||||
const downloaded = receiver.waitForEvent('download');
|
||||
await request.getByRole('button', { name: '承認', exact: true }).click();
|
||||
const file = await downloaded;
|
||||
assert.equal(file.suggestedFilename(), filename);
|
||||
assert.deepEqual(await readFile(await file.path()), bytes);
|
||||
await sender.locator('.send-dialog').waitFor({ state: 'hidden' });
|
||||
|
||||
await sender.locator('.login-button').click();
|
||||
await sender.getByRole('button', { name: 'Save to library', exact: true }).count();
|
||||
await sender.getByRole('tab', { name: 'Saved', exact: true }).click();
|
||||
await sender.getByRole('button', { name: 'Save to library', exact: true }).click();
|
||||
await sender.getByRole('button', { name: 'Save clipboard content', exact: true }).click();
|
||||
await sender.getByLabel('Text to send', { exact: true }).fill('Keep this content unchanged');
|
||||
await sender.getByRole('button', { name: 'Save', exact: true }).click();
|
||||
await sender.locator('.item-card').filter({ hasText: 'Keep this content unchanged' }).waitFor();
|
||||
await expect(sender.locator('.item-footer')).toContainText('[Expires]');
|
||||
await language(sender, 'ja');
|
||||
await expect(sender.locator('.item-footer')).toContainText('[保管期限]');
|
||||
assert.equal(await sender.locator('.item-body').innerText(), 'Keep this content unchanged');
|
||||
await sender.screenshot({ path: 'tests/e2e/i18n-library-ja.png', fullPage: true });
|
||||
await language(sender, 'en');
|
||||
await sender.screenshot({ path: 'tests/e2e/i18n-library-en.png', fullPage: true });
|
||||
await sender.getByRole('button', { name: 'More', exact: true }).click();
|
||||
await sender.getByRole('menuitem', { name: 'Administrator', exact: true }).click();
|
||||
await expect(sender.locator('.admin-panel')).toHaveAttribute('lang', 'ko');
|
||||
await expect(sender.locator('.admin-panel h1')).toHaveText('서비스 관리');
|
||||
await expect(sender.locator('html')).toHaveAttribute('lang', 'en');
|
||||
|
||||
// The Japanese login form and server-origin error are also localized.
|
||||
await receiver.locator('.login-button').click();
|
||||
await receiver.setViewportSize({ width: 320, height: 900 });
|
||||
await noKoreanUI(receiver);
|
||||
await receiver.getByLabel('メールアドレス', { exact: true }).fill('missing@hop.test');
|
||||
await receiver.getByLabel('パスワード', { exact: true }).fill('12345678');
|
||||
await receiver
|
||||
.locator('.account-auth')
|
||||
.getByRole('button', { name: 'ログイン', exact: true })
|
||||
.click();
|
||||
await expect(receiver.locator('.error-toast')).toContainText(
|
||||
'メールアドレスとパスワードを確認してください。',
|
||||
);
|
||||
await receiver.screenshot({ path: 'tests/e2e/i18n-login-ja.png', fullPage: true });
|
||||
await receiver.evaluate(() => navigator.serviceWorker.ready);
|
||||
await receiver.context().setOffline(true);
|
||||
await receiver.reload({ waitUntil: 'domcontentloaded' });
|
||||
await expect(receiver.getByText('インターネット接続が必要です。', { exact: true })).toBeVisible();
|
||||
await expect(receiver.locator('html')).toHaveAttribute('lang', 'ja');
|
||||
await receiver.context().setOffline(false);
|
||||
assert.deepEqual(errors, []);
|
||||
console.log(
|
||||
'PASS: trusted country/browser defaults and explicit preference priority/race/reload, cross-tab language changes preserve device/draft, EN registration/mail/verification/account settings, EN→JA rooms/text/copy/link decline/file download, live localized library/dates/errors/manifest, Korean-only administrator and mobile layouts.',
|
||||
);
|
||||
} finally {
|
||||
await browser?.close();
|
||||
await runtime.app.close();
|
||||
await rm(folder, { recursive: true, force: true });
|
||||
}
|
||||
@@ -31,11 +31,13 @@ try {
|
||||
});
|
||||
const origin = { latitude: 37.5665, longitude: 126.978, accuracy: 20 };
|
||||
const a = await browser.newContext({
|
||||
locale: 'ko-KR',
|
||||
permissions: ['geolocation'],
|
||||
geolocation: origin,
|
||||
viewport: { width: 1280, height: 900 },
|
||||
});
|
||||
const b = await browser.newContext({
|
||||
locale: 'ko-KR',
|
||||
permissions: ['geolocation'],
|
||||
geolocation: { ...origin, latitude: origin.latitude + 0.0004 },
|
||||
viewport: { width: 390, height: 844 },
|
||||
|
||||
@@ -33,8 +33,9 @@ try {
|
||||
? { executablePath: process.env.HOP_BROWSER_EXECUTABLE }
|
||||
: {}),
|
||||
});
|
||||
const a = await browser.newContext({ viewport: { width: 1440, height: 1050 } }),
|
||||
const a = await browser.newContext({ locale: 'ko-KR', viewport: { width: 1440, height: 1050 } }),
|
||||
b = await browser.newContext({
|
||||
locale: 'ko-KR',
|
||||
viewport: { width: 390, height: 844 },
|
||||
isMobile: true,
|
||||
hasTouch: true,
|
||||
@@ -314,7 +315,7 @@ try {
|
||||
await sender.close();
|
||||
await receiver.getByRole('button', { name: 'hop. 홈', exact: true }).click();
|
||||
await receiver.locator('.peer-card').filter({ hasText: '보내는 Mac' }).waitFor();
|
||||
const c = await browser.newContext({ viewport: { width: 1366, height: 900 } }),
|
||||
const c = await browser.newContext({ locale: 'ko-KR', viewport: { width: 1366, height: 900 } }),
|
||||
urlReceiver = await c.newPage();
|
||||
urlReceiver.on('pageerror', (error) => errors.push(error.message));
|
||||
await urlReceiver.goto(url);
|
||||
|
||||
@@ -42,8 +42,9 @@ try {
|
||||
? { executablePath: process.env.HOP_BROWSER_EXECUTABLE }
|
||||
: {}),
|
||||
});
|
||||
const a = await browser.newContext({ viewport: { width: 1440, height: 1000 } }),
|
||||
const a = await browser.newContext({ locale: 'ko-KR', viewport: { width: 1440, height: 1000 } }),
|
||||
b = await browser.newContext({
|
||||
locale: 'ko-KR',
|
||||
viewport: { width: 390, height: 844 },
|
||||
isMobile: true,
|
||||
hasTouch: true,
|
||||
|
||||
@@ -29,6 +29,7 @@ try {
|
||||
: {}),
|
||||
});
|
||||
const context = await browser.newContext({
|
||||
locale: 'ko-KR',
|
||||
viewport: { width: 390, height: 844 },
|
||||
userAgent:
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 Version/18.0 Mobile/15E148 Safari/604.1',
|
||||
|
||||
@@ -32,8 +32,8 @@ try {
|
||||
? { executablePath: process.env.HOP_BROWSER_EXECUTABLE }
|
||||
: {}),
|
||||
});
|
||||
const a = await browser.newContext({ viewport: { width: 1280, height: 900 } }),
|
||||
b = await browser.newContext({ viewport: { width: 390, height: 844 } });
|
||||
const a = await browser.newContext({ locale: 'ko-KR', viewport: { width: 1280, height: 900 } }),
|
||||
b = await browser.newContext({ locale: 'ko-KR', viewport: { width: 390, height: 844 } });
|
||||
const sender = await a.newPage(),
|
||||
receiver = await b.newPage(),
|
||||
errors = [];
|
||||
|
||||
@@ -32,8 +32,9 @@ try {
|
||||
? { executablePath: process.env.HOP_BROWSER_EXECUTABLE }
|
||||
: {}),
|
||||
});
|
||||
const a = await browser.newContext({ viewport: { width: 1280, height: 900 } }),
|
||||
const a = await browser.newContext({ locale: 'ko-KR', viewport: { width: 1280, height: 900 } }),
|
||||
b = await browser.newContext({
|
||||
locale: 'ko-KR',
|
||||
viewport: { width: 390, height: 844 },
|
||||
isMobile: true,
|
||||
hasTouch: true,
|
||||
|
||||
Reference in New Issue
Block a user