Apply supplied SVG branding and polish device settings
@@ -98,3 +98,7 @@ QR·4자리 코드는 기본 10분의 임시 공간 초대입니다. 초대 코
|
||||
로그인 계정의 임시 저장과 받은 파일을 합쳐 개인별 2GiB로 제한합니다. 같은 원본은 한 계정에서 한 번 계산하고 다른 계정의 한도와 분리합니다. 한도가 차면 파일 수신·다운로드는 계속하고 받은 항목에 저장하지 않았다는 안내를 표시합니다. 익명 업로드는 기기 세션당 2GiB, 서버 전체 20GiB이며 화면에는 내역을 표시하지 않습니다.
|
||||
|
||||
근처 위치 검색과 기종 구분은 [기기 발견 문서](docs/NEARBY_DISCOVERY.md)를 참고하세요. 양쪽 기기가 위치를 허용하면 약 100m 내 기기를 표시하며, 대기 화면은 반복 목록 검색을 하지 않습니다.
|
||||
|
||||
로고 원본은 `apps/web/public/logo.svg`, 아이콘 원본은 `apps/web/public/icon.svg`입니다. 헤더·서비스 정보 로고는 SVG 경로를 사용하므로 기기 기본 폰트에 영향을 받지 않습니다. 흰 글자는 테마에 맞춰 변경하고 청록색 점은 유지합니다. 아이콘 원본을 교체한 뒤 `node scripts/generate-icons.mjs`를 실행하면 192px·512px·Android maskable PNG가 다시 생성됩니다.
|
||||
|
||||
기기 이름은 서버 DB에 저장되며 동일 브라우저의 기기 세션 쿠키가 유효한 동안 유지됩니다. 쿠키를 지우거나 다른 브라우저로 접속하면 새 기기로 인식됩니다.
|
||||
|
||||
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 10 KiB |
@@ -1 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="16" fill="#111111"/><path d="M18 14v36h8V34c0-8 12-8 12 0v16h8V32c0-15-13-17-20-9v-9z" fill="#fff"/><circle cx="52" cy="48" r="4" fill="#ffffff"/></svg>
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1_6)">
|
||||
<path d="M48 0H16C7.16344 0 0 7.16344 0 16V48C0 56.8366 7.16344 64 16 64H48C56.8366 64 64 56.8366 64 48V16C64 7.16344 56.8366 0 48 0Z" fill="#111111"/>
|
||||
<path d="M18 14V50H26V34C26 26 38 26 38 34V50H46V32C46 17 33 15 26 23V14H18Z" fill="white"/>
|
||||
<path d="M52 52C54.2091 52 56 50.2091 56 48C56 45.7909 54.2091 44 52 44C49.7909 44 48 45.7909 48 48C48 50.2091 49.7909 52 52 52Z" fill="#00FFFF"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1_6">
|
||||
<rect width="64" height="64" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 246 B After Width: | Height: | Size: 629 B |
11
apps/web/public/logo.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="60" height="30" viewBox="0 0 60 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2_2)">
|
||||
<path d="M56.07 23.6023C55.2518 23.6023 54.5511 23.3144 53.9677 22.7386C53.392 22.1629 53.1079 21.4659 53.1155 20.6477C53.1079 19.8447 53.392 19.1591 53.9677 18.5909C54.5511 18.0152 55.2518 17.7273 56.07 17.7273C56.8427 17.7273 57.5245 18.0152 58.1155 18.5909C58.7139 19.1591 59.017 19.8447 59.0245 20.6477C59.017 21.1932 58.873 21.6894 58.5927 22.1364C58.32 22.5833 57.9602 22.9394 57.5132 23.2045C57.0738 23.4697 56.5927 23.6023 56.07 23.6023Z" fill="#00FFFF"/>
|
||||
<path d="M5.55682 13.3182V23.2727H0V0H5.375V9.02273H5.56818C5.96212 7.94697 6.60606 7.10606 7.5 6.5C8.40152 5.89394 9.50379 5.59091 10.8068 5.59091C12.0341 5.59091 13.1023 5.86364 14.0114 6.40909C14.9205 6.94697 15.625 7.70833 16.125 8.69318C16.6326 9.67803 16.8826 10.8295 16.875 12.1477V23.2727H11.3182V13.2386C11.3258 12.2689 11.0833 11.5114 10.5909 10.9659C10.0985 10.4205 9.4053 10.1477 8.51136 10.1477C7.92803 10.1477 7.41288 10.2765 6.96591 10.5341C6.52652 10.7841 6.18182 11.1439 5.93182 11.6136C5.68939 12.0833 5.56439 12.6515 5.55682 13.3182ZM25.9315 23.6023C24.0981 23.6023 22.5224 23.2273 21.2042 22.4773C19.8936 21.7197 18.8822 20.6667 18.1701 19.3182C17.4656 17.9621 17.1133 16.3902 17.1133 14.6023C17.1133 12.8068 17.4656 11.2348 18.1701 9.88636C18.8822 8.5303 19.8936 7.47727 21.2042 6.72727C22.5224 5.9697 24.0981 5.59091 25.9315 5.59091C27.7648 5.59091 29.3368 5.9697 30.6474 6.72727C31.9656 7.47727 32.9769 8.5303 33.6815 9.88636C34.3936 11.2348 34.7497 12.8068 34.7497 14.6023C34.7497 16.3902 34.3936 17.9621 33.6815 19.3182C32.9769 20.6667 31.9656 21.7197 30.6474 22.4773C29.3368 23.2273 27.7648 23.6023 25.9315 23.6023ZM25.9656 19.4091C26.6322 19.4091 27.1966 19.2045 27.6588 18.7955C28.1209 18.3864 28.4731 17.8182 28.7156 17.0909C28.9656 16.3636 29.0906 15.5227 29.0906 14.5682C29.0906 13.5985 28.9656 12.75 28.7156 12.0227C28.4731 11.2955 28.1209 10.7273 27.6588 10.3182C27.1966 9.90909 26.6322 9.70455 25.9656 9.70455C25.2762 9.70455 24.6928 9.90909 24.2156 10.3182C23.7459 10.7273 23.386 11.2955 23.136 12.0227C22.8936 12.75 22.7724 13.5985 22.7724 14.5682C22.7724 15.5227 22.8936 16.3636 23.136 17.0909C23.386 17.8182 23.7459 18.3864 24.2156 18.7955C24.6928 19.2045 25.2762 19.4091 25.9656 19.4091ZM35.0363 29.8182V5.81818H40.5363V8.80682H40.7067C40.934 8.27652 41.2559 7.76515 41.6726 7.27273C42.0969 6.7803 42.6347 6.37879 43.2863 6.06818C43.9453 5.75 44.7332 5.59091 45.6499 5.59091C46.862 5.59091 47.9946 5.90909 49.0476 6.54545C50.1082 7.18182 50.9643 8.16288 51.6158 9.48864C52.2673 10.8144 52.5931 12.5038 52.5931 14.5568C52.5931 16.5341 52.2787 18.1894 51.6499 19.5227C51.0287 20.8561 50.1878 21.8561 49.1272 22.5227C48.0741 23.1894 46.9037 23.5227 45.6158 23.5227C44.737 23.5227 43.9756 23.3788 43.3317 23.0909C42.6878 22.803 42.1461 22.4242 41.7067 21.9545C41.2749 21.4848 40.9416 20.9811 40.7067 20.4432H40.5931V29.8182H35.0363ZM40.4794 14.5455C40.4794 15.4848 40.6044 16.303 40.8544 17C41.112 17.697 41.4794 18.2386 41.9567 18.625C42.4416 19.0038 43.0211 19.1932 43.6953 19.1932C44.3772 19.1932 44.9567 19.0038 45.434 18.625C45.9113 18.2386 46.2711 17.697 46.5135 17C46.7635 16.303 46.8885 15.4848 46.8885 14.5455C46.8885 13.6061 46.7635 12.7917 46.5135 12.1023C46.2711 11.4129 45.9113 10.8788 45.434 10.5C44.9643 10.1212 44.3847 9.93182 43.6953 9.93182C43.0135 9.93182 42.434 10.1174 41.9567 10.4886C41.4794 10.8598 41.112 11.3902 40.8544 12.0795C40.6044 12.7689 40.4794 13.5909 40.4794 14.5455Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2_2">
|
||||
<rect width="60" height="30" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
@@ -40,6 +40,7 @@ import { version as appVersion } from '../package.json';
|
||||
import AccountPanel from './AccountPanel.vue';
|
||||
import SendComposer from './SendComposer.vue';
|
||||
import InstallButton from './InstallButton.vue';
|
||||
import LogoMark from './LogoMark.vue';
|
||||
import { createNearbySearch } from './nearbySearch';
|
||||
import { useVisibleViewport } from './viewport';
|
||||
useVisibleViewport();
|
||||
@@ -631,7 +632,7 @@ async function spaceAction(space, action, memberId) {
|
||||
<header class="site-header">
|
||||
<div class="header-brand">
|
||||
<button class="wordmark" @click="tab = 'devices'" aria-label="hop. 홈">
|
||||
hop<span>.</span>
|
||||
<LogoMark />
|
||||
</button>
|
||||
<span class="header-tagline">기기 사이를 가볍게.</span>
|
||||
</div>
|
||||
@@ -1129,7 +1130,7 @@ async function spaceAction(space, action, memberId) {
|
||||
</template>
|
||||
<template v-else-if="modal === 'about'">
|
||||
<div class="service-info">
|
||||
<strong class="service-brand">hop.</strong>
|
||||
<LogoMark class="service-brand" label="hop." />
|
||||
<p class="service-tagline">기기 사이를 가볍게.</p>
|
||||
<div class="service-intro">
|
||||
<p>Android, iOS, macOS, Windows.</p>
|
||||
@@ -1171,17 +1172,28 @@ async function spaceAction(space, action, memberId) {
|
||||
aria-label="기기 이름"
|
||||
maxlength="40"
|
||||
required /></label
|
||||
><button class="button primary" :disabled="busy" aria-label="이름 저장">
|
||||
이름 저장
|
||||
</button>
|
||||
><button class="button primary" :disabled="busy" aria-label="저장">저장</button>
|
||||
</form>
|
||||
<label class="settings-row discovery-setting"
|
||||
><span>다른 기기에서 발견 허용</span
|
||||
><input
|
||||
type="checkbox"
|
||||
:checked="device?.discoverable"
|
||||
:disabled="busy"
|
||||
@change="act(() => patchDevice({ discoverable: $event.target.checked }))" /></label
|
||||
<label class="settings-row discovery-setting">
|
||||
<span>다른 기기에서 발견 허용</span>
|
||||
<span class="relative inline-flex h-6 w-11 shrink-0 items-center">
|
||||
<input
|
||||
class="peer absolute inset-0 m-0 h-full w-full cursor-pointer opacity-0"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
:checked="device?.discoverable"
|
||||
:disabled="busy"
|
||||
@change="act(() => patchDevice({ discoverable: $event.target.checked }))"
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="pointer-events-none absolute inset-0 rounded-full bg-[var(--line)] transition-colors motion-reduce:transition-none peer-checked:bg-[var(--ink)] peer-focus-visible:outline-2 peer-focus-visible:outline-offset-4 peer-focus-visible:outline-[var(--ink)] peer-disabled:opacity-50"
|
||||
></span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="pointer-events-none absolute left-0.5 top-0.5 size-5 rounded-full bg-[var(--surface)] shadow-sm transition-transform motion-reduce:transition-none peer-checked:translate-x-5 peer-disabled:opacity-50"
|
||||
></span>
|
||||
</span> </label
|
||||
></template>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
20
apps/web/src/LogoMark.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script setup>
|
||||
import { useId } from 'vue';
|
||||
import source from '../public/logo.svg?raw';
|
||||
defineProps({ label: String });
|
||||
const instance = useId();
|
||||
// A fixed repository asset, not user/API HTML. Keep each SVG's clip IDs unique.
|
||||
const markup = source
|
||||
.replace(/fill="(?:white|#fff|#ffffff)"/gi, 'fill="currentColor"')
|
||||
.replace(/\bid="([^"]+)"/g, (_, id) => `id="${id}-${instance}"`)
|
||||
.replace(/url\(#([^)]+)\)/g, (_, id) => `url(#${id}-${instance})`);
|
||||
</script>
|
||||
<template>
|
||||
<span
|
||||
class="brand-logo"
|
||||
:role="label ? 'img' : undefined"
|
||||
:aria-label="label"
|
||||
:aria-hidden="label ? undefined : 'true'"
|
||||
v-html="markup"
|
||||
></span>
|
||||
</template>
|
||||
@@ -116,17 +116,14 @@ button:active:not(:disabled) {
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.wordmark {
|
||||
font-size: 43px;
|
||||
font-weight: 750;
|
||||
letter-spacing: -3.5px;
|
||||
display: block;
|
||||
width: 64px;
|
||||
height: 32px;
|
||||
border: 0;
|
||||
background: none;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
.wordmark span {
|
||||
color: var(--brand);
|
||||
}
|
||||
.desktop-nav {
|
||||
display: flex;
|
||||
gap: 34px;
|
||||
@@ -1257,9 +1254,6 @@ h1 span {
|
||||
height: 80px;
|
||||
gap: 0;
|
||||
}
|
||||
.wordmark {
|
||||
font-size: 37px;
|
||||
}
|
||||
.desktop-nav {
|
||||
display: none;
|
||||
}
|
||||
@@ -2893,9 +2887,6 @@ a.button {
|
||||
.site-header {
|
||||
gap: 18px;
|
||||
}
|
||||
.site-header .wordmark {
|
||||
font-size: 32px;
|
||||
}
|
||||
.header-brand {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
@@ -3035,7 +3026,8 @@ a.button {
|
||||
gap: 10px;
|
||||
}
|
||||
.site-header .wordmark {
|
||||
font-size: 28px;
|
||||
width: 56px;
|
||||
height: 28px;
|
||||
}
|
||||
.device-footer {
|
||||
align-items: flex-start;
|
||||
@@ -3447,8 +3439,9 @@ a.button {
|
||||
font-size: 13px;
|
||||
}
|
||||
.service-brand {
|
||||
font-size: 30px;
|
||||
letter-spacing: -0.07em;
|
||||
display: block;
|
||||
width: 64px;
|
||||
height: 32px;
|
||||
}
|
||||
.service-info p {
|
||||
margin: 0;
|
||||
@@ -3500,3 +3493,48 @@ a.button {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
display: block;
|
||||
color: var(--ink);
|
||||
}
|
||||
.wordmark .brand-logo {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.brand-logo svg {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.modal .device-settings-form {
|
||||
height: auto;
|
||||
align-items: flex-end;
|
||||
}
|
||||
.modal .device-settings-form input,
|
||||
.modal .device-settings-form .button {
|
||||
height: 44px;
|
||||
min-height: 44px;
|
||||
border-radius: 12px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
.modal .device-settings-form .button {
|
||||
min-width: 72px;
|
||||
}
|
||||
.modal .device-settings-form input:focus-visible {
|
||||
outline-offset: -2px;
|
||||
}
|
||||
.modal .discovery-setting {
|
||||
border: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.modal .discovery-setting input {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@media (max-width: 639px) {
|
||||
.modal .device-settings-form {
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,3 +113,11 @@
|
||||
- 서비스 정보는 hop. 제목을 유지하고 운영체제 설명과 아이콘 목록으로 정리했다. 제작사명은 왼쪽, 웹 패키지 버전(v0.1.0)은 오른쪽에 표시한다. PC·모바일 화면을 확인했다. 연결 참여/만들기 폼의 설명 여백과 간격을 줄이고 기기 카드의 온라인 표시는 제거했으며 승인 대기 표시는 유지했다.
|
||||
|
||||
- 기기 목록의 수동 새로고침 버튼을 제거했다. 접속 이벤트에 따른 목록 자동 갱신은 유지한다. 사용자가 수정한 헤더 버튼 간격과 서비스 정보 줄 간격도 함께 반영했다.
|
||||
|
||||
## 2026-10-12 사용자 로고·아이콘과 기기 설정 마무리
|
||||
|
||||
- 사용자가 넣은 logo.svg를 헤더와 서비스 정보에 적용했다. 폰트 대신 SVG 경로를 사용하고 흰 글자를 테마 색상으로 바꾸며 청록색 점은 유지한다. 동시에 표시되는 SVG clipPath ID도 분리한다.
|
||||
- 새 icon.svg로 192px·512px PNG와 Android maskable PNG를 재생성했다. maskable은 중앙 안전 영역에 글자와 점이 들어오도록 여백을 넣는다. 재생성 스크립트를 추가했다.
|
||||
- 발견 허용은 실제 체크박스 입력과 switch 역할을 유지한 Tailwind 토글로 표시한다. 키보드 포커스·비활성·동작 줄이기 상태를 지원하고 아래 구분선을 제거했다.
|
||||
- 이름 저장 버튼은 ‘저장’으로 변경하고 입력창과 44px 높이·둥근 모서리를 맞췄다. PC 폼의 이전 30px 고정 높이를 없애 제목과 입력창이 겹치지 않도록 했다.
|
||||
- 전체 빌드, PC/모바일 레이아웃·테마별 로고 색·기기명 유지·토글 및 방 관리·파일/링크 전송·PWA 브라우저 검증을 통과했다. PNG 크기·청록색 점 보존과 maskable의 불투명 배경·중앙 안전 영역도 확인했다. 실제 Galaxy/iPhone의 설치 아이콘 반영은 운영 재배포 후 확인한다.
|
||||
|
||||
22
scripts/generate-icons.mjs
Normal file
@@ -0,0 +1,22 @@
|
||||
import { createRequire } from 'node:module';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
const require = createRequire(new URL('../apps/server/package.json', import.meta.url));
|
||||
const sharp = require('sharp');
|
||||
const asset = (name) => fileURLToPath(new URL(`../apps/web/public/${name}`, import.meta.url));
|
||||
const source = await readFile(asset('icon.svg'));
|
||||
await Promise.all(
|
||||
[192, 512].map((size) =>
|
||||
sharp(source, { density: (72 * size) / 64 })
|
||||
.resize(size, size)
|
||||
.png()
|
||||
.toFile(asset(`icon-${size}.png`)),
|
||||
),
|
||||
);
|
||||
// Leave margin so the letter and dot remain inside Android's central safe zone.
|
||||
const padded = await sharp(source, { density: 432 }).resize(384, 384).png().toBuffer();
|
||||
await sharp({ create: { width: 512, height: 512, channels: 4, background: '#111111' } })
|
||||
.composite([{ input: padded, left: 64, top: 64 }])
|
||||
.png()
|
||||
.toFile(asset('icon-maskable.png'));
|
||||
console.log('Generated icon-192.png, icon-512.png, icon-maskable.png from icon.svg');
|
||||
@@ -43,7 +43,7 @@ try {
|
||||
async function rename(page, name) {
|
||||
await page.locator('.device-name').click();
|
||||
await page.getByRole('textbox', { name: '기기 이름' }).fill(name);
|
||||
await page.getByRole('button', { name: '이름 저장' }).click();
|
||||
await page.getByRole('button', { name: '저장' }).click();
|
||||
await page.locator('.device-name').filter({ hasText: name }).waitFor();
|
||||
}
|
||||
await rename(host, '방장 Mac');
|
||||
|
||||
@@ -46,6 +46,24 @@ try {
|
||||
});
|
||||
await page.goto(url);
|
||||
await page.locator('.app-shell[data-online="true"]').waitFor();
|
||||
await page.locator('.wordmark .brand-logo svg').waitFor();
|
||||
const letterColor = () =>
|
||||
page
|
||||
.locator('.wordmark path[fill="currentColor"]')
|
||||
.first()
|
||||
.evaluate((el) => getComputedStyle(el).fill);
|
||||
const dotColor = () =>
|
||||
page.locator('.wordmark path[fill="#00FFFF"]').evaluate((el) => getComputedStyle(el).fill);
|
||||
assert.equal(await letterColor(), 'rgb(245, 245, 245)');
|
||||
assert.equal(await dotColor(), 'rgb(0, 255, 255)');
|
||||
await page.getByRole('button', { name: '더보기', exact: true }).click();
|
||||
await page.getByRole('menuitem', { name: '라이트 테마로 전환', exact: true }).click();
|
||||
assert.equal(await letterColor(), 'rgb(23, 23, 23)');
|
||||
assert.equal(await dotColor(), 'rgb(0, 255, 255)');
|
||||
await page.screenshot({ path: 'tests/e2e/brand-light.png' });
|
||||
await page.getByRole('button', { name: '더보기', exact: true }).click();
|
||||
await page.getByRole('menuitem', { name: '다크 테마로 전환', exact: true }).click();
|
||||
|
||||
assert.equal(
|
||||
await page
|
||||
.locator('.header-actions > .theme-toggle,.header-actions > .pwa-install-button')
|
||||
@@ -64,6 +82,15 @@ try {
|
||||
await more.click();
|
||||
await page.getByRole('menuitem', { name: '서비스 정보', exact: true }).click();
|
||||
await page.getByRole('dialog', { name: '서비스 정보', exact: true }).waitFor();
|
||||
const clipIds = await page
|
||||
.locator('.brand-logo clipPath')
|
||||
.evaluateAll((nodes) => nodes.map((node) => node.id));
|
||||
assert.equal(
|
||||
new Set(clipIds).size,
|
||||
clipIds.length,
|
||||
'동시에 표시한 로고의 clipPath ID가 겹치지 않음',
|
||||
);
|
||||
|
||||
assert.equal(await more.getAttribute('aria-expanded'), 'false');
|
||||
await page.screenshot({ path: 'tests/e2e/service-info-desktop.png' });
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
@@ -205,6 +232,18 @@ try {
|
||||
.evaluate((el) => getComputedStyle(el).fontSize),
|
||||
'16px',
|
||||
);
|
||||
const mobileInput = await page.getByLabel('기기 이름', { exact: true }).boundingBox();
|
||||
const mobileSave = await settings
|
||||
.getByRole('button', { name: '저장', exact: true })
|
||||
.boundingBox();
|
||||
assert.equal(mobileInput.height, mobileSave.height);
|
||||
await page.getByRole('switch', { name: '다른 기기에서 발견 허용' }).waitFor();
|
||||
assert.equal(
|
||||
await page
|
||||
.locator('.discovery-setting')
|
||||
.evaluate((el) => getComputedStyle(el).borderBottomWidth),
|
||||
'0px',
|
||||
);
|
||||
const settingsBounds = await settings.boundingBox();
|
||||
assert(Math.abs(settingsBounds.y + settingsBounds.height - 900) < 2, '모바일 설정 하단 시트');
|
||||
await page.getByLabel('기기 이름', { exact: true }).focus();
|
||||
@@ -261,8 +300,22 @@ try {
|
||||
await page.keyboard.press('Escape');
|
||||
await sheet.waitFor({ state: 'hidden' });
|
||||
await page.locator('.footer-device-summary').click();
|
||||
const deviceModal = page.getByRole('dialog', { name: '설정', exact: true });
|
||||
const titleBounds = await deviceModal.locator('header').boundingBox();
|
||||
const formBounds = await deviceModal.locator('.device-settings-form').boundingBox();
|
||||
assert(
|
||||
formBounds.y >= titleBounds.y + titleBounds.height + 12,
|
||||
'제목과 기기 이름 폼이 겹치지 않음',
|
||||
);
|
||||
const desktopInput = await page.getByLabel('기기 이름', { exact: true }).boundingBox();
|
||||
const desktopSave = await deviceModal
|
||||
.getByRole('button', { name: '저장', exact: true })
|
||||
.boundingBox();
|
||||
assert.equal(desktopInput.height, desktopSave.height);
|
||||
assert(Math.abs(desktopInput.y - desktopSave.y) < 1, 'PC 입력창과 버튼이 같은 위치에 정렬됨');
|
||||
await page.screenshot({ path: 'tests/e2e/device-settings-desktop.png' });
|
||||
await page.getByLabel('기기 이름', { exact: true }).fill('나의 테스트 기기');
|
||||
await page.getByRole('button', { name: '이름 저장', exact: true }).click();
|
||||
await page.getByRole('button', { name: '저장', exact: true }).click();
|
||||
await page.locator('.footer-device-name').filter({ hasText: '나의 테스트 기기' }).waitFor();
|
||||
await page.locator('.footer-device-summary').click();
|
||||
await page.getByLabel('다른 기기에서 발견 허용').uncheck();
|
||||
|
||||
@@ -81,7 +81,7 @@ try {
|
||||
await receiver.locator('.app-shell[data-online="true"]').waitFor();
|
||||
await sender.locator('.device-name').click();
|
||||
await sender.getByRole('textbox', { name: '기기 이름' }).fill('보내는 Mac');
|
||||
await sender.getByRole('button', { name: '이름 저장' }).click();
|
||||
await sender.getByRole('button', { name: '저장' }).click();
|
||||
await sender.locator('.device-name').filter({ hasText: '보내는 Mac' }).waitFor();
|
||||
await sender.getByRole('button', { name: '다른 기기 연결', exact: true }).click();
|
||||
await sender.getByRole('button', { name: '연결 공간 만들기' }).click();
|
||||
@@ -401,7 +401,7 @@ try {
|
||||
assert.equal(await second.locator('.device-name').innerText(), oldDevice);
|
||||
await second.locator('.device-name').click();
|
||||
await second.getByRole('textbox', { name: '기기 이름' }).fill('재연결된 Mac');
|
||||
await second.getByRole('button', { name: '이름 저장' }).click();
|
||||
await second.getByRole('button', { name: '저장' }).click();
|
||||
await second.locator('.device-name').filter({ hasText: '재연결된 Mac' }).waitFor();
|
||||
assert.deepEqual(errors, []);
|
||||
console.log(
|
||||
|
||||