Clarify transfer rejections and improve storage readability

This commit is contained in:
2026-10-15 17:18:07 +09:00
parent 60a14e9c6c
commit 8fb8c203f0
17 changed files with 300 additions and 55 deletions

View File

@@ -160,7 +160,40 @@ try {
await admin.locator('.admin-file').filter({ hasText: 'sample.mp4' }).waitFor();
assert.match(await admin.locator('.admin-file').innerText(), /중국\(CN\) · Beijing/);
assert.match(await admin.locator('.admin-file').innerText(), /1\.1\.1\.1/);
assert.match(await admin.locator('.admin-file-owner').innerText(), /^[a-f0-9-]{36}\s+비로그인$/);
assert.match(
await admin.locator('.admin-file-owner').innerText(),
/^[a-f0-9-]{36}\s*·\s*게스트\(Guest\)$/,
);
assert.match(await admin.locator('.admin-file').innerText(), /Beijing\s*·\s*1\.1\.1\.1/);
const sizeStyle = await admin
.locator('.admin-stats article')
.nth(1)
.locator('strong .file-size')
.evaluate((label) => {
const fraction = label.querySelector('.file-size-fraction');
const whole = label.querySelector('.file-size-whole');
const unit = label.querySelector('.file-size-unit');
return {
whole: whole.textContent,
fraction: fraction.textContent,
unit: unit.textContent,
dimmed: Number(getComputedStyle(fraction).opacity) < 1,
smaller:
parseFloat(getComputedStyle(fraction).fontSize) <
parseFloat(getComputedStyle(whole).fontSize),
inline: getComputedStyle(fraction).display === 'inline',
unitGap: parseFloat(getComputedStyle(unit).marginInlineStart) > 0,
};
});
assert.deepEqual(sizeStyle, {
whole: '7,687',
fraction: '.0',
unit: 'GB',
dimmed: true,
smaller: true,
inline: true,
unitGap: true,
});
assert.doesNotMatch(await admin.locator('.admin-file-owner').innerText(), /guest-|user-/);
assert.match(await admin.locator('.admin-file').innerText(), /접속 지역 · 중국\(CN\) · Beijing/);
assert.doesNotMatch(await admin.locator('.admin-file').innerText(), /최근 접속/);
@@ -475,7 +508,7 @@ try {
const ownedCard = admin.locator('.admin-file').filter({ hasText: 'account-owner.txt' });
await ownedCard.waitFor();
const ownerLabel = await ownedCard.locator('.admin-file-owner').innerText();
assert.match(ownerLabel, /^[a-f0-9-]{36}\s+other@hop\.test$/);
assert.match(ownerLabel, /^[a-f0-9-]{36}\s*·\s*other@hop\.test$/);
assert.doesNotMatch(ownerLabel, /user-|guest-|\[/);
await admin.screenshot({ path: 'tests/e2e/admin-file-account.png', fullPage: true });
await admin.getByRole('button', { name: '관리 기록', exact: true }).click();

View File

@@ -280,6 +280,14 @@ try {
await storedLinkPage.waitForURL('https://example.com/saved');
await storedLinkPage.close();
await receiver.locator('.item-body').filter({ hasText: 'https://example.com/saved' }).waitFor();
await sender.locator('.send-dialog').waitFor({ state: 'hidden' });
await sender.locator('.peer-card').first().click();
await sender.getByRole('button', { name: '보관함에서 가져오기', exact: true }).click();
await sender.locator('.send-dialog .vault-choice').filter({ hasText: '업무 링크' }).click();
await sender.getByRole('button', { name: '전송하기', exact: true }).click();
await receiver.getByRole('button', { name: '거절', exact: true }).click();
await sender.locator('.send-dialog').waitFor({ state: 'hidden' });
await sender.locator('.toast').filter({ hasText: '전송을 거절했어요.' }).waitFor();
await navigate(sender, '보관함');
await sender.getByRole('button', { name: '보관함에 담기', exact: true }).click();
await sender

View File

@@ -194,9 +194,52 @@ try {
await expect(blocked.getByText('차단한 기기가 없어요.')).toBeVisible();
await expect(receiver.locator('.peer-card')).toHaveCount(1);
assert.equal((await api(receiver, '/device/blocks')).blocks.length, 0);
await blocked.getByRole('button', { name: '대화상자 닫기', exact: true }).click();
const recipientName = (await api(receiver, '/device/me')).device.name;
// Reject the first request before its POST response reaches the sender. This
// must still close the composer even if it never observed a pending snapshot.
await sender.route('**/api/transfer-requests', async (route) => {
const response = await route.fetch();
assert.equal(response.status(), 200);
const { requestId } = await response.json();
await api(receiver, `/transfer-requests/${requestId}/reject`, 'POST', {});
await route.fulfill({ response });
});
for (const kind of ['text', 'file', 'url']) {
await sender.locator('.peer-card').first().click();
if (kind === 'file')
await sender.getByLabel('보낼 파일 선택', { exact: true }).setInputFiles({
name: 'declined.txt',
mimeType: 'text/plain',
buffer: Buffer.from('test'),
});
else
await sender
.getByLabel('보낼 텍스트', { exact: true })
.fill(kind === 'url' ? 'https://example.com/declined' : '빠른 거절도 놓치지 않습니다.');
await sender.getByRole('button', { name: '전송하기', exact: true }).click();
if (kind !== 'text') {
const incoming = receiver.getByRole('dialog', { name: '전송 수락 요청' });
await incoming.getByRole('button', { name: '거절', exact: true }).click();
}
await expect(sender.locator('.send-dialog')).toHaveCount(0);
await expect(sender.locator('.toast')).toHaveText(`${recipientName}에서 전송을 거절했어요.`);
if (kind === 'text') await sender.unroute('**/api/transfer-requests');
}
// History must not replay rejection notifications after a reload; the fourth
// request now gets the real pair cooldown and visible remaining seconds.
await sender.reload();
await sender.locator('.peer-card').first().waitFor();
await expect(sender.locator('.toast')).toHaveCount(0);
await sender.locator('.peer-card').first().click();
await sender.getByLabel('보낼 텍스트', { exact: true }).fill('세 번 거절 이후의 대기');
await sender.getByRole('button', { name: '전송하기', exact: true }).click();
await expect(sender.locator('.content-submit')).toBeDisabled();
await expect(sender.locator('.content-submit')).toHaveText(/\d+초 후 전송 가능/);
await expect(sender.locator('.error-toast')).toContainText('연속 3회 거절');
assert.deepEqual(errors, []);
console.log(
'PASS: content icons/titles, desktop/mobile 2:1 buttons, consistent single file sizes 97.7KB/1.3MB/1.6GB, no list indent or overflow, retry countdown and draft preservation, block confirmation and private unblock UI.',
'PASS: receive icons/sizes, desktop/mobile 2:1 controls, retry countdown/draft preservation, block/unblock, first two rejections allow retries, text/file/link rejection closes sender and notifies, decision before HTTP response and reload deduplication, third rejection triggers pair cooldown.',
);
} finally {
await browser?.close();