CI / Build & Quality Checks (push) Skipped
CI / Secret scan (gitleaks) (push) Skipped
CI / Playwright smoke (e2e) (push) Skipped
CI / Build & Quality Checks (pull_request) Successful in 3m38s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 14s
CI / Playwright smoke (e2e) (pull_request) Successful in 22m30s
Settings -> General -> "Your data": Export writes one JSON file of the account's Lotus data (synced settings, bookmarks, private notes, room names and sections, reminders, status presets, thread notification settings, timed mutes, soundboard (clips stay on the server), emoji packs, avatar decoration, and this device's settings). Not messages or encryption keys: they have their own exports, and keys are secret. Import merges: new items are added; things that differ are listed per category and kept unless the user picks "Use the file's". Only our own account-data types are read from a file. Files from a newer Lotus Chat are refused with a plain message; the version stamp leaves room to migrate older ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
99 lines
4.2 KiB
TypeScript
99 lines
4.2 KiB
TypeScript
import fs from 'node:fs';
|
|
import { test, expect } from '@playwright/test';
|
|
import { api, ensureUser, hsReachable, loginUI, uniq } from './localHs';
|
|
|
|
// [Gitea #114] Export / import "your data": a round trip through the file, the
|
|
// merge summary, and the keep-mine / use-the-file's choice.
|
|
const enc = encodeURIComponent;
|
|
|
|
test.describe('your data (#114)', () => {
|
|
test.beforeAll(async () => {
|
|
test.skip(!(await hsReachable()), 'local homeserver not running');
|
|
});
|
|
|
|
test('export, change things, import merges with the chosen conflict rule', async ({ page }) => {
|
|
test.setTimeout(150_000);
|
|
const user = await ensureUser(uniq('yd'));
|
|
const put = (type: string, content: unknown) =>
|
|
api(
|
|
'PUT',
|
|
`/_matrix/client/v3/user/${enc(user.userId)}/account_data/${type}`,
|
|
user.token,
|
|
content,
|
|
);
|
|
const get = (type: string) =>
|
|
api<Record<string, unknown>>(
|
|
'GET',
|
|
`/_matrix/client/v3/user/${enc(user.userId)}/account_data/${type}`,
|
|
user.token,
|
|
);
|
|
const bm = (eventId: string) => ({ roomId: '!r:localhost', eventId, savedAt: 1 });
|
|
await put('io.lotus.bookmarks', { bookmarks: [bm('$one'), bm('$two')] });
|
|
await put('io.lotus.user_notes', { '@friend:localhost': 'met at the LAN party' });
|
|
|
|
await loginUI(page, user);
|
|
await page
|
|
.getByText('Connecting...')
|
|
.first()
|
|
.waitFor({ state: 'detached', timeout: 90_000 })
|
|
.catch(() => undefined);
|
|
const openGeneral = async () => {
|
|
await page.getByRole('button', { name: 'User Settings' }).click();
|
|
await page.getByRole('dialog').getByText('General', { exact: true }).first().click();
|
|
await page
|
|
.getByRole('dialog')
|
|
.getByText('Your data', { exact: true })
|
|
.scrollIntoViewIfNeeded();
|
|
};
|
|
await openGeneral();
|
|
|
|
// Export.
|
|
const [download] = await Promise.all([
|
|
page.waitForEvent('download'),
|
|
page.getByRole('dialog').getByRole('button', { name: 'Export', exact: true }).click(),
|
|
]);
|
|
const path = await download.path();
|
|
const file = JSON.parse(fs.readFileSync(path!, 'utf8'));
|
|
expect(file.format).toBe('lotus-chat-data');
|
|
expect(file.accountData['io.lotus.bookmarks'].bookmarks).toHaveLength(2);
|
|
expect(file.local.settings).toBeTruthy();
|
|
|
|
// Lose a bookmark, change the note.
|
|
await put('io.lotus.bookmarks', { bookmarks: [bm('$one')] });
|
|
await put('io.lotus.user_notes', { '@friend:localhost': 'changed my mind' });
|
|
await page.waitForTimeout(3000); // let sync deliver the new account data
|
|
|
|
const importFile = async () => {
|
|
await page
|
|
.locator('input[type="file"][aria-label="Lotus Chat data file"]')
|
|
.setInputFiles(path!);
|
|
await expect(page.getByRole('region', { name: 'Import summary' })).toBeVisible();
|
|
};
|
|
await importFile();
|
|
const summary = page.getByRole('region', { name: 'Import summary' });
|
|
await expect(summary).toContainText('Bookmarks: 1 new');
|
|
await expect(summary).toContainText('Private notes about people: 1 differ');
|
|
|
|
// Keep mine: the bookmark comes back, the note stays as it is now.
|
|
await summary.getByRole('button', { name: 'Import', exact: true }).click();
|
|
await expect(page.getByText('Imported.')).toBeVisible({ timeout: 20_000 });
|
|
expect(((await get('io.lotus.bookmarks')).bookmarks as unknown[]).length).toBe(2);
|
|
expect((await get('io.lotus.user_notes'))['@friend:localhost']).toBe('changed my mind');
|
|
|
|
// Again, using the file's: the note goes back.
|
|
await page.waitForTimeout(2000);
|
|
await importFile();
|
|
await summary.getByText("Use the file's").click();
|
|
await summary.getByRole('button', { name: 'Import', exact: true }).click();
|
|
await expect(page.getByText('Imported.')).toBeVisible({ timeout: 20_000 });
|
|
expect((await get('io.lotus.user_notes'))['@friend:localhost']).toBe('met at the LAN party');
|
|
|
|
// Wrong files are refused with a plain message.
|
|
fs.writeFileSync(`${path}.bad`, '{"format":"something-else"}');
|
|
await page
|
|
.locator('input[type="file"][aria-label="Lotus Chat data file"]')
|
|
.setInputFiles(`${path}.bad`);
|
|
await expect(page.getByText("This isn't a Lotus Chat data file.")).toBeVisible();
|
|
});
|
|
});
|