Simplify connection flow, mobile transfer UI, and header actions

This commit is contained in:
2026-10-12 18:14:54 +09:00
parent 5f431ee153
commit 4f0231da3d
22 changed files with 820 additions and 131 deletions

View File

@@ -48,18 +48,20 @@ try {
}
await rename(host, '방장 Mac');
await rename(guest, '외부 기기');
await host.getByRole('button', { name: 'QR · 코드로 연결', exact: true }).click();
await host.getByRole('checkbox', { name: '참여할 때 내 승인 받기' }).check();
await host.getByRole('button', { name: '다른 기기 연결', exact: true }).click();
await host.getByRole('button', { name: '연결 공간 만들기' }).click();
await host.getByRole('checkbox', { name: '참여할 때 내 승인 받기' }).check();
await host.getByRole('button', { name: '만들기', exact: true }).click();
await host.locator('.invite-code').waitFor();
const code = (await host.locator('.invite-code').innerText()).replace(/\s/g, '');
await host.getByRole('button', { name: '대화상자 닫기' }).click();
await host.locator('.my-device-info .host-badge').waitFor();
await guest.getByRole('button', { name: 'QR · 코드로 연결', exact: true }).click();
await guest.getByRole('button', { name: '다른 기기 연결', exact: true }).click();
await guest.getByRole('button', { name: '연결 공간 참여', exact: true }).click();
await guest.getByRole('textbox', { name: '연결 코드' }).fill(code);
await guest.getByRole('button', { name: '참여', exact: true }).click();
await guest.getByRole('button', { name: '참여하기', exact: true }).click();
await guest.locator('.room-leave').waitFor();
await guest.getByRole('dialog', { name: 'QR · 코드로 연결' }).waitFor({ state: 'hidden' });
await guest.locator('.modal-backdrop').waitFor({ state: 'hidden' });
const guestCard = host.locator('.peer-entry').filter({ hasText: '외부 기기' });
await guestCard.getByText('승인 대기', { exact: true }).waitFor();
assert.equal(await guestCard.locator('.peer-card').isDisabled(), true);

View File

@@ -46,6 +46,27 @@ try {
});
await page.goto(url);
await page.locator('.app-shell[data-online="true"]').waitFor();
assert.equal(
await page
.locator('.header-actions > .theme-toggle,.header-actions > .pwa-install-button')
.count(),
0,
);
const more = page.getByRole('button', { name: '더보기', exact: true });
await more.click();
await page.getByRole('menu', { name: '더보기', exact: true }).waitFor();
await page.keyboard.press('End');
assert.equal(await page.evaluate(() => document.activeElement.textContent.trim()), '서비스 정보');
await page.keyboard.press('ArrowUp');
assert.match(await page.evaluate(() => document.activeElement.textContent.trim()), /테마/);
await page.keyboard.press('Escape');
assert.equal(await more.getAttribute('aria-expanded'), 'false');
await more.click();
await page.getByRole('menuitem', { name: '서비스 정보', exact: true }).click();
await page.getByRole('dialog', { name: '서비스 정보', exact: true }).waitFor();
assert.equal(await more.getAttribute('aria-expanded'), 'false');
await page.getByRole('button', { name: '대화상자 닫기', exact: true }).click();
assert.equal(await page.getByText('어디로 보낼까요?', { exact: true }).count(), 0);
assert.equal(await page.locator('.devices-stage .spin,.section-heading h2 span').count(), 0);
await page.screenshot({ path: 'tests/e2e/main-empty-device.png', fullPage: true });
@@ -165,6 +186,65 @@ try {
assert.equal(linkBounds.width, bounds.width);
await page.screenshot({ path: 'tests/e2e/send-sheet.png' });
await page.getByRole('button', { name: '전송 화면 닫기' }).click();
await page.locator('.footer-device-summary').click();
const settings = page.getByRole('dialog', { name: '설정', exact: true });
assert.equal(
await page
.getByLabel('기기 이름', { exact: true })
.evaluate((el) => document.activeElement === el),
false,
'모바일 설정에서 키보드를 자동으로 열지 않음',
);
assert.equal(
await page
.getByLabel('기기 이름', { exact: true })
.evaluate((el) => getComputedStyle(el).fontSize),
'16px',
);
const settingsBounds = await settings.boundingBox();
assert(Math.abs(settingsBounds.y + settingsBounds.height - 900) < 2, '모바일 설정 하단 시트');
await page.getByLabel('기기 이름', { exact: true }).focus();
await page.evaluate(() => {
Object.defineProperty(visualViewport, 'height', { configurable: true, value: 480 });
visualViewport.dispatchEvent(new Event('resize'));
});
await page.waitForFunction(
() => document.documentElement.style.getPropertyValue('--hop-visible-height') === '480px',
);
const keyboardBounds = await settings.boundingBox();
assert(
keyboardBounds.y >= 0 && Math.abs(keyboardBounds.y + keyboardBounds.height - 480) < 2,
'키보드가 열린 가시 영역 안의 설정 시트',
);
await page.screenshot({ path: 'tests/e2e/device-settings-mobile-keyboard.png' });
await page.evaluate(() => {
delete visualViewport.height;
visualViewport.dispatchEvent(new Event('resize'));
});
await page.getByRole('button', { name: '대화상자 닫기', exact: true }).click();
await page.getByRole('button', { name: '로그인', exact: false }).first().click();
for (const input of [
page.getByLabel('이메일', { exact: true }),
page.getByLabel('비밀번호', { exact: true }),
]) {
assert.equal(await input.evaluate((el) => getComputedStyle(el).fontSize), '16px');
await input.focus();
assert.equal(
await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth),
true,
);
}
await page.screenshot({ path: 'tests/e2e/login-mobile.png' });
await more.click();
const menuBounds = await page.getByRole('menu', { name: '더보기', exact: true }).boundingBox();
assert(
menuBounds.x >= 0 && menuBounds.x + menuBounds.width <= 390,
'모바일 더보기 메뉴 가로 넘침 없음',
);
await page.screenshot({ path: 'tests/e2e/header-more-mobile.png' });
await page.keyboard.press('Escape');
await page.getByRole('button', { name: 'hop. 홈', exact: true }).click();
await page.setViewportSize({ width: 1280, height: 1000 });
await page.locator('.peer-card').first().click();
const dialog = await page.locator('.send-dialog').boundingBox();
@@ -186,8 +266,32 @@ try {
await page.getByLabel('다른 기기에서 발견 허용').check();
await page.locator('.footer-device-summary').filter({ hasText: '발견 허용' }).waitFor();
await page.getByRole('button', { name: '대화상자 닫기', exact: true }).click();
await page.getByRole('button', { name: 'QR · 코드로 연결', exact: true }).click();
await page.getByRole('button', { name: '연결 공간 만들기' }).click();
await page.getByRole('button', { name: '다른 기기 연결', exact: true }).click();
assert.equal(await page.locator('.connection-choice').count(), 2);
assert.equal(await page.getByRole('checkbox', { name: '참여할 때 내 승인 받기' }).count(), 0);
assert.equal(await page.getByLabel('연결 코드', { exact: true }).count(), 0);
await page.screenshot({ path: 'tests/e2e/connection-choose-desktop.png' });
await page.setViewportSize({ width: 390, height: 844 });
await page.getByRole('button', { name: '연결 공간 참여', exact: true }).click();
const joinInput = page.getByLabel('연결 코드', { exact: true });
assert.equal(await joinInput.getAttribute('placeholder'), '초대 코드 4자리');
assert.equal(await joinInput.evaluate((el) => getComputedStyle(el).fontSize), '16px');
await joinInput.fill('ABC');
assert.equal(
await page.getByRole('button', { name: '참여하기', exact: true }).isDisabled(),
true,
);
await joinInput.fill('ABCD');
assert.equal(await page.getByRole('button', { name: '참여하기', exact: true }).isEnabled(), true);
await page.screenshot({ path: 'tests/e2e/connection-join-mobile.png' });
await page.getByRole('button', { name: '연결 방법 선택으로 돌아가기', exact: true }).click();
await page.getByRole('button', { name: '연결 공간 만들기', exact: true }).click();
await page.getByRole('checkbox', { name: '참여할 때 내 승인 받기' }).waitFor();
assert.equal(await page.getByLabel('연결 코드', { exact: true }).count(), 0);
await page.getByRole('button', { name: '만들기', exact: true }).click();
assert.match(await page.locator('.invite-code').innerText(), /^[A-HJ-NP-Z2-9]{4}$/);
assert.equal(await page.locator('.local-qr-note,.invite-display .spin').count(), 0);
await page.screenshot({ path: 'tests/e2e/connection-invite-mobile.png' });
const code = (await page.locator('.invite-code').innerText()).replace(/\s/g, '');
const cookie = sockets[1].io.opts.extraHeaders.Cookie;
const token = cookie.split('=')[1];
@@ -202,13 +306,10 @@ try {
body: JSON.stringify({ code }),
});
assert.equal(response.status, 200);
await page.getByRole('dialog', { name: 'QR · 코드로 연결' }).waitFor({ state: 'hidden' });
await page.locator('.modal-backdrop').waitFor({ state: 'hidden' });
await page.waitForFunction(() => document.querySelectorAll('.peer-card').length === 1);
await page.screenshot({ path: 'tests/e2e/main-single-device.png', fullPage: true });
assert.equal(
await page.getByRole('button', { name: 'QR · 코드로 연결', exact: true }).count(),
0,
);
assert.equal(await page.getByRole('button', { name: '다른 기기 연결', exact: true }).count(), 0);
await page.getByRole('button', { name: '방 나가기', exact: true }).click();
await page.waitForFunction(() => document.querySelectorAll('.peer-card').length === 12);
assert.deepEqual(errors, []);

View File

@@ -167,7 +167,7 @@ try {
'권한 거절 후 반복 요청 없음',
);
assert.equal(
await receiver.getByRole('button', { name: 'QR · 코드로 연결', exact: true }).count(),
await receiver.getByRole('button', { name: '다른 기기 연결', exact: true }).count(),
1,
);
assert.deepEqual(await sender.evaluate(() => caches.keys()), []);

View File

@@ -83,16 +83,18 @@ try {
await sender.getByRole('textbox', { name: '기기 이름' }).fill('보내는 Mac');
await sender.getByRole('button', { name: '이름 저장' }).click();
await sender.locator('.device-name').filter({ hasText: '보내는 Mac' }).waitFor();
await sender.getByRole('button', { name: 'QR · 코드로 연결', exact: true }).click();
await sender.getByRole('button', { name: '다른 기기 연결', exact: true }).click();
await sender.getByRole('button', { name: '연결 공간 만들기' }).click();
await sender.getByRole('button', { name: '만들기', exact: true }).click();
await sender.locator('.invite-code').waitFor();
const code = (await sender.locator('.invite-code').innerText()).replace(/\s/g, '');
await receiver.getByRole('button', { name: 'QR · 코드로 연결', exact: true }).click();
await receiver.getByRole('button', { name: '다른 기기 연결', exact: true }).click();
await receiver.getByRole('button', { name: '연결 공간 참여', exact: true }).click();
await receiver.getByRole('textbox', { name: '연결 코드' }).fill(code);
await receiver.getByRole('button', { name: '참여', exact: true }).click();
await receiver.getByRole('button', { name: '참여하기', exact: true }).click();
await receiver.locator('.room-leave').waitFor();
await sender.getByRole('dialog', { name: 'QR · 코드로 연결' }).waitFor({ state: 'hidden' });
await receiver.getByRole('dialog', { name: 'QR · 코드로 연결' }).waitFor({ state: 'hidden' });
await sender.locator('.modal-backdrop').waitFor({ state: 'hidden' });
await receiver.locator('.modal-backdrop').waitFor({ state: 'hidden' });
await sender.locator('.peer-card').first().waitFor();
await sender.locator('.peer-card').first().click();
const secret = 'https://example.com/?q=%3Cscript%3Ealert(1)%3C%2Fscript%3E';
@@ -262,6 +264,13 @@ try {
uploadCaptured,
new Promise((_, reject) => setTimeout(() => reject(Error('업로드 시작 대기 실패')), 10000)),
]);
await sender.setViewportSize({ width: 390, height: 844 });
await sender.locator('.file-transfer-progress progress').waitFor();
const listText = await sender.locator('.content-pane').innerText();
assert.equal(listText.split('browser.txt').length - 1, 1, '파일명은 목록에 한 번만 표시');
assert(!listText.includes('전송 중입니다.'), '진행률이 있으면 전송 중 안내를 반복하지 않음');
assert.equal(await sender.locator('.composer-progress').count(), 0);
await sender.screenshot({ path: 'tests/e2e/file-progress-mobile.png' });
await sender.locator('.send-button').filter({ hasText: '전송 취소' }).click();
releaseUpload();
await receiver.locator('.receiving-card').waitFor({ state: 'hidden' });
@@ -277,14 +286,17 @@ try {
await sender.evaluate(() => getComputedStyle(document.documentElement).backgroundColor),
'rgb(0, 0, 0)',
);
await sender.setViewportSize({ width: 1440, height: 1050 });
await sender.screenshot({ path: 'tests/e2e/desktop.png', fullPage: true });
await sender.getByRole('button', { name: '라이트 테마로 전환', exact: true }).click();
await sender.getByRole('button', { name: '더보기', exact: true }).click();
await sender.getByRole('menuitem', { name: '라이트 테마로 전환', exact: true }).click();
assert.equal(
await sender.evaluate(() => getComputedStyle(document.documentElement).backgroundColor),
'rgb(250, 250, 250)',
);
await sender.screenshot({ path: 'tests/e2e/light-desktop.png', fullPage: true });
await sender.getByRole('button', { name: '다크 테마로 전환', exact: true }).click();
await sender.getByRole('button', { name: '더보기', exact: true }).click();
await sender.getByRole('menuitem', { name: '다크 테마로 전환', exact: true }).click();
await receiver.screenshot({ path: 'tests/e2e/mobile.png', fullPage: true });
assert.equal(
await receiver.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth),
@@ -303,11 +315,12 @@ try {
await urlReceiver.goto(url);
await urlReceiver.locator('.device-name').waitFor();
const receiverCode = (await urlReceiver.locator('.footer-device-code').innerText()).trim();
await urlReceiver.getByRole('button', { name: 'QR · 코드로 연결', exact: true }).click();
await urlReceiver.getByRole('button', { name: '다른 기기 연결', exact: true }).click();
await urlReceiver.getByRole('button', { name: '연결 공간 참여', exact: true }).click();
await urlReceiver.getByRole('textbox', { name: '연결 코드' }).fill(code);
await urlReceiver.getByRole('button', { name: '참여', exact: true }).click();
await urlReceiver.getByRole('button', { name: '참여하기', exact: true }).click();
await urlReceiver.locator('.room-leave').waitFor();
await urlReceiver.getByRole('dialog', { name: 'QR · 코드로 연결' }).waitFor({ state: 'hidden' });
await urlReceiver.locator('.modal-backdrop').waitFor({ state: 'hidden' });
await second.locator('.peer-card').filter({ hasText: receiverCode }).waitFor();
await second.evaluate(() => {
window.__hopClipboard = '일반 텍스트는 전송하지 않습니다.';

View File

@@ -94,15 +94,17 @@ try {
await navigate(sender, '기기');
await navigate(receiver, '기기');
assert.equal(await sender.locator('.hero').count(), 0);
await sender.getByRole('button', { name: 'QR · 코드로 연결', exact: true }).click();
await sender.getByRole('button', { name: '다른 기기 연결', exact: true }).click();
await sender.getByRole('button', { name: '연결 공간 만들기' }).click();
await sender.getByRole('button', { name: '만들기', exact: true }).click();
const code = (await sender.locator('.invite-code').innerText()).replace(/\s/g, '');
await sender.getByRole('button', { name: '대화상자 닫기' }).click();
await receiver.getByRole('button', { name: 'QR · 코드로 연결', exact: true }).click();
await receiver.getByRole('button', { name: '다른 기기 연결', exact: true }).click();
await receiver.getByRole('button', { name: '연결 공간 참여', exact: true }).click();
await receiver.getByRole('textbox', { name: '연결 코드' }).fill(code);
await receiver.getByRole('button', { name: '참여', exact: true }).click();
await receiver.getByRole('button', { name: '참여하기', exact: true }).click();
await receiver.locator('.room-leave').waitFor();
await receiver.getByRole('dialog', { name: 'QR · 코드로 연결' }).waitFor({ state: 'hidden' });
await receiver.locator('.modal-backdrop').waitFor({ state: 'hidden' });
await sender.locator('.peer-card').first().click();
await sender.getByRole('button', { name: '대신 링크 붙여넣기', exact: true }).click();
await sender.getByLabel('보낼 URL').fill('https://example.com/received');

View File

@@ -35,9 +35,16 @@ try {
});
const page = await context.newPage();
await page.addInitScript(() => {
window.addEventListener('beforeinstallprompt', (event) => {
if (!event.__hopTest) { event.preventDefault(); event.stopImmediatePropagation(); }
}, { capture: true });
window.addEventListener(
'beforeinstallprompt',
(event) => {
if (!event.__hopTest) {
event.preventDefault();
event.stopImmediatePropagation();
}
},
{ capture: true },
);
});
await page.goto(url);
await page.locator('.app-shell[data-online="true"]').waitFor();
@@ -48,7 +55,8 @@ try {
assert.equal((await context.request.get(url + icon.src)).status(), 200);
await page.waitForFunction(() => navigator.serviceWorker.controller !== null);
assert.deepEqual(await page.evaluate(() => caches.keys()), []);
const install = page.getByRole('button', { name: 'hop. 앱 설치', exact: true });
await page.getByRole('button', { name: '더보기', exact: true }).click();
const install = page.getByRole('menuitem', { name: 'hop. 앱 설치', exact: true });
await install.click();
await page.getByRole('dialog', { name: '앱 설치 안내' }).waitFor();
assert.match(await page.locator('.install-help').innerText(), /Safari.*공유/s);
@@ -67,6 +75,7 @@ try {
event.userChoice = Promise.resolve({ outcome: 'accepted' });
window.dispatchEvent(event);
});
await page.getByRole('button', { name: '더보기', exact: true }).click();
await install.click();
assert.equal(await page.evaluate(() => window.__installCalls), 1);
await install.waitFor({ state: 'hidden' });