Add plain text clipboard transfers and refine input feedback

This commit is contained in:
2026-10-13 14:07:03 +09:00
parent af004a2942
commit 8a8df417aa
32 changed files with 1263 additions and 197 deletions

View File

@@ -107,6 +107,11 @@ try {
window.__hopClipboard = 'https://example.com/focus-refresh';
window.dispatchEvent(new Event('focus'));
});
await sender.waitForFunction(
(value) => document.querySelector('.current-url')?.value === value,
secret,
);
await sender.getByRole('button', { name: '다시 가져오기', exact: true }).click();
await sender.waitForFunction(
(value) => document.querySelector('.current-url')?.value === value,
'https://example.com/focus-refresh',
@@ -123,7 +128,7 @@ try {
await sender.evaluate(() => {
window.__hopClipboard = 'https://example.com/?q=%3Cscript%3Ealert(1)%3C%2Fscript%3E';
});
await sender.getByRole('button', { name: '클립보드 가져오기', exact: true }).click();
await sender.getByRole('button', { name: '다시 가져오기', exact: true }).click();
await sender.waitForFunction(
(value) => document.querySelector('.current-url')?.value === value,
secret,
@@ -218,7 +223,7 @@ try {
window.__hopClipboardDenied = true;
});
await sender.locator('.peer-card').first().click();
await sender.getByRole('button', { name: '파일 선택', exact: true }).waitFor();
await sender.getByLabel('보낼 텍스트', { exact: true }).waitFor();
assert.equal(await sender.locator('.clipboard-link-card').count(), 0);
const fileBytes = Buffer.from('브라우저 파일 전달 검증');
const dragged = await sender.evaluateHandle(() => {
@@ -326,16 +331,29 @@ try {
window.__hopClipboard = '일반 텍스트는 전송하지 않습니다.';
});
await second.locator('.peer-card').filter({ hasText: receiverCode }).click();
await second.getByRole('button', { name: '파일 선택', exact: true }).waitFor();
assert.equal(
await second.locator('.clipboard-link-card').count(),
0,
'일반 텍스트를 링크로 제안하면 안 됨',
);
await second.getByRole('button', { name: '대신 링크 붙여넣기', exact: true }).click();
await second.getByLabel('보낼 텍스트', { exact: true }).waitFor();
const urls = ['https://example.com/path?x=1', 'http://192.168.1.10/private'];
await second.getByRole('textbox', { name: '보낼 URL' }).fill(urls.join('\n'));
await second.getByRole('button', { name: '전송하기', exact: true }).click();
await second.getByRole('button', { name: '전송 화면 닫기', exact: true }).click();
// Old API callers may still send multiple URLs; the new clipboard UI treats these as text.
const sourceCookie = (await a.cookies(url)).find((cookie) => cookie.name === 'hop_guest');
const targetCookie = (await c.cookies(url)).find((cookie) => cookie.name === 'hop_guest');
const targetGuest = runtime.service.guest(targetCookie.value);
const legacyHeaders = {
Origin: url,
'X-CSRF-Token': runtime.service.csrf(sourceCookie.value),
'Idempotency-Key': crypto.randomUUID(),
};
const legacyRequest = await a.request.post(`${url}/api/transfer-requests`, {
headers: legacyHeaders,
data: {
targetDeviceId: runtime.service.device(targetGuest.id).id,
kind: 'url',
count: 2,
byteSize: Buffer.byteLength(urls.join('\n')),
},
});
assert.equal(legacyRequest.status(), 200);
const legacyId = (await legacyRequest.json()).requestId;
await urlReceiver.getByRole('button', { name: '수락', exact: true }).waitFor();
assert(!(await urlReceiver.locator('body').innerText()).includes(urls[0]));
await c.route('https://example.com/**', (route) =>
@@ -347,9 +365,11 @@ try {
const opened = [];
urlReceiver.on('popup', (page) => opened.push(page));
await urlReceiver.getByRole('button', { name: '수락', exact: true }).click();
await second
.getByRole('button', { name: '전송 취소', exact: false })
.waitFor({ state: 'hidden' });
const legacyComplete = await a.request.post(`${url}/api/transfers/${legacyId}/complete`, {
headers: { ...legacyHeaders, 'Idempotency-Key': crypto.randomUUID() },
data: { urls },
});
assert.equal(legacyComplete.status(), 200);
assert.equal(opened.length, 2);
await Promise.all(
opened.map((page) => page.waitForURL((address) => urls.includes(address.href))),
@@ -365,8 +385,8 @@ try {
window.open = () => null;
});
await second.locator('.peer-card').filter({ hasText: receiverCode }).click();
await second.getByRole('button', { name: '대신 링크 붙여넣기', exact: true }).click();
await second.getByRole('textbox', { name: '보낼 URL' }).fill('https://example.com/blocked');
await second.locator('.current-url, .current-text').fill('https://example.com/blocked');
await second.getByRole('button', { name: '전송하기', exact: true }).click();
await urlReceiver.getByRole('button', { name: '수락', exact: true }).click();
await urlReceiver.locator('.delivery-blocked').waitFor();
@@ -405,7 +425,7 @@ try {
await second.locator('.device-name').filter({ hasText: '재연결된 Mac' }).waitFor();
assert.deepEqual(errors, []);
console.log(
'PASS: 두 브라우저 QR·코드 연결, 수락 전 본문 비공개, 클립보드 URL·복수 URL 전달과 일반 텍스트 무시, 수락 후 자동 새 탭 URL 열기, opener 격리, 읽음 동기화, 이름 유지, 다중 탭·CSRF 재연결 복구, 390px 가로 넘침 없음, 실제 파일 업로드·자동 다운로드, 수락 중 지연된 과거 조회에도 전달 완료, 스크롤 뒤 중앙의 고대비 수락 팝업, 검정/라이트 전환, 버튼 자리의 전송 취소·익명 내역 숨김·새로고침 중복 실행 방지.',
'PASS: 두 브라우저 QR·코드 연결, 수락 전 본문 비공개, 클립보드 URL·기존 복수 URL API 호환과 클립보드 입력, 수락 후 자동 새 탭 URL 열기, opener 격리, 읽음 동기화, 이름 유지, 다중 탭·CSRF 재연결 복구, 390px 가로 넘침 없음, 실제 파일 업로드·자동 다운로드, 수락 중 지연된 과거 조회에도 전달 완료, 스크롤 뒤 중앙의 고대비 수락 팝업, 검정/라이트 전환, 버튼 자리의 전송 취소·익명 내역 숨김·새로고침 중복 실행 방지.',
);
} catch (error) {
console.error(error);