Use visible NAS upload storage and reconcile missing files

This commit is contained in:
2026-10-13 15:54:27 +09:00
parent 50e54bc4c5
commit 9e0f493228
26 changed files with 1075 additions and 119 deletions

View File

@@ -1,6 +1,6 @@
import assert from 'node:assert/strict';
import { chromium } from '@playwright/test';
import { mkdtemp, rm } from 'node:fs/promises';
import { mkdtemp, rm, readdir, unlink } from 'node:fs/promises';
import { resolve } from 'node:path';
import { tmpdir } from 'node:os';
import { loadConfig } from '../../apps/server/src/config.ts';
@@ -80,6 +80,10 @@ try {
await auth.getByLabel('비밀번호', { exact: true }).fill('1234');
await auth.getByRole('button', { name: '로그인', exact: true }).click();
await page.locator('.account-workspace').waitFor();
assert.deepEqual(
(await page.getByRole('tab').allTextContents()).map((text) => text.trim()),
['받은 항목', '임시 저장'],
);
assert.equal(
await page.getByRole('tab', { name: '받은 항목', exact: true }).getAttribute('aria-selected'),
'true',
@@ -109,6 +113,11 @@ try {
await page.keyboard.press('Tab');
assert.equal(await preferences.locator('select,input').count(), 0, '항목 목록을 먼저 표시');
assert.equal(await preferences.locator('.preference-item').count(), 2);
assert.equal(
await preferences.getByRole('button', { name: '이메일 변경', exact: true }).count(),
0,
'이메일 변경 버튼 숨김',
);
assert.equal(
await preferences.locator('.preference-item').first().locator('p').innerText(),
email,
@@ -146,20 +155,19 @@ try {
}
await page.screenshot({ path: `tests/e2e/account-password-${device}.png` });
await preferences.getByRole('button', { name: '계정 설정 목록으로', exact: true }).click();
await preferences.getByRole('button', { name: '이메일 변경', exact: true }).click();
await preferences.getByRole('button', { name: '비밀번호 변경', exact: true }).click();
assert.equal(
await preferences.getByLabel('현재 비밀번호', { exact: true }).inputValue(),
'',
'항목 전환 시 비밀번호 초안 제거',
);
await preferences.getByLabel('새 이메일', { exact: true }).fill('new@example.com');
await page.screenshot({ path: `tests/e2e/account-email-${device}.png` });
await preferences.getByLabel('새 비밀번호', { exact: true }).fill('draft1234');
await page.keyboard.press('Escape');
await preferences.waitFor({ state: 'hidden' });
await page.getByRole('button', { name: '계정 설정', exact: true }).click();
await preferences.getByRole('button', { name: '이메일 변경', exact: true }).click();
await preferences.getByRole('button', { name: '비밀번호 변경', exact: true }).click();
assert.equal(
await preferences.getByLabel('새 이메일', { exact: true }).inputValue(),
await preferences.getByLabel('새 비밀번호', { exact: true }).inputValue(),
'',
'모달 닫기 시 개인정보 초안 제거',
);
@@ -220,6 +228,10 @@ try {
await sender.locator('.current-url, .current-text').fill('https://example.com/saved');
await sender.getByRole('button', { name: '저장하기', exact: true }).click();
await sender.locator('.item-card').filter({ hasText: '나중에 읽을 글' }).waitFor();
assert.match(
await sender.locator('.item-footer > span').first().innerText(),
/^보관 만료 · .+까지$/,
);
await sender.getByRole('button', { name: '메모 수정', exact: true }).click();
await sender.getByLabel('한 줄 메모 수정').fill('업무 링크');
await sender.locator('.note-editor').getByRole('button', { name: '저장', exact: true }).click();
@@ -300,12 +312,74 @@ try {
);
const saved = await sender.evaluate(async () => (await (await fetch('/api/vault')).json()).items);
assert(saved.every((item) => item.expiresAt - item.createdAt <= 7 * 86400000));
await navigate(receiver, '기기');
await receiver.locator('.peer-card').first().click();
await receiver.getByRole('button', { name: '보관함에서 가져오기', exact: true }).click();
await receiver.locator('.vault-choice').filter({ hasText: 'saved.txt' }).click();
await receiver.locator('.current-vault').waitFor();
assert.equal(await receiver.locator('.content-submit').isEnabled(), true);
let removed = false;
for (const key of await readdir(runtime.service.storage.originals)) {
const path = resolve(runtime.service.storage.originals, key);
if ((await readFile(path)).equals(bytes)) {
await unlink(path);
removed = true;
break;
}
}
assert(removed, '보관함 원본을 서버 저장소에서 직접 삭제');
let unexpectedDownloads = 0;
sender.on('download', () => unexpectedDownloads++);
await fileRow.getByRole('button', { name: '다운로드', exact: true }).click();
await sender.locator('.error-toast').filter({ hasText: '다시 전송' }).waitFor();
const toastAlignment = await sender.locator('.error-toast').evaluate((element) => {
const toast = element.getBoundingClientRect(),
text = element.querySelector('span').getBoundingClientRect(),
icon = element.querySelector('button svg').getBoundingClientRect();
return {
left: text.left - toast.left,
right: toast.right - icon.right,
textCenter: text.top + text.height / 2,
iconCenter: icon.top + icon.height / 2,
};
});
assert(
Math.abs(toastAlignment.left - toastAlignment.right) < 1,
'오류 텍스트와 X 아이콘의 양쪽 여백 균형',
);
assert(
Math.abs(toastAlignment.textCenter - toastAlignment.iconCenter) < 1,
'오류 텍스트와 X 아이콘의 세로 정렬',
);
await sender.screenshot({ path: 'tests/e2e/account-missing-file.png', fullPage: true });
assert.equal(unexpectedDownloads, 0, '없는 파일을 다운로드하기 전에 안내');
await fileRow.waitFor({ state: 'hidden' });
await receiver.locator('.current-vault').waitFor({ state: 'hidden' });
await receiver
.locator('.vault-choice')
.filter({ hasText: 'saved.txt' })
.waitFor({ state: 'hidden' });
assert.equal(
await receiver.locator('.content-submit').isDisabled(),
true,
'다른 기기의 열린 전송 선택도 삭제 알림으로 해제',
);
await receiver.getByRole('button', { name: '전송 화면 닫기', exact: true }).click();
await navigate(receiver, '받은 항목');
for (const page of [sender, receiver])
assert.equal(
await page.evaluate(
async () => (await (await fetch('/api/account/storage')).json()).usedBytes,
),
0,
);
await sender.locator('.error-toast').waitFor({ state: 'hidden', timeout: 8000 });
await receiver.getByRole('button', { name: '로그아웃', exact: true }).click();
await receiver.locator('.account-auth').waitFor();
assert(!(await receiver.locator('body').innerText()).includes('https://example.com/saved'));
assert.deepEqual(errors, []);
console.log(
'PASS: 받은 항목 기본 화면·보관 안내, PC/모바일 계정 설정 목록·항목별 폼·간격·16px 입력·초안 제거·키보드 탐색, 가입·인증·로그인, 날짜별 기록, 직접 링크/파일 저장과 메모 수정, 보관함 자료의 수락 후 자동 열기/다운로드, 파일 재업로드 없음, 7일 만료, 삭제/복원, 로그아웃 권한 격리. 실제 Resend 발송은 포함하지 않음.',
'PASS: 받은 항목 기본 화면·보관 안내, PC/모바일 계정 설정·입력·키보드 탐색, 가입·인증·로그인, 날짜별 기록, 직접 링크/파일 저장·메모·수락 후 자동 열기/다운로드, 파일 재업로드 없음, 7일 만료·삭제/복원·로그아웃 권한 격리, 원본 수동 삭제 시 다운로드 전 안내·양쪽 목록/용량 정리·열린 전송 선택 해제. 실제 Resend 발송은 포함하지 않음.',
);
} catch (error) {
console.error(error);