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>( '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(); }); });