647 lines
24 KiB
JavaScript
647 lines
24 KiB
JavaScript
const {test, expect: baseExpect} = require('@playwright/test');
|
|
const fs = require('fs').promises;
|
|
const path = require('path');
|
|
const crypto = require('crypto');
|
|
|
|
const expect = baseExpect.extend({
|
|
toBeNumberOrNull(received) {
|
|
const pass = received === null || (typeof received === 'number' && !isNaN(received));
|
|
|
|
if (pass) {
|
|
return {
|
|
message: () => `expected ${received} not to be a number or null`,
|
|
pass: true,
|
|
};
|
|
} else {
|
|
return {
|
|
message: () => `expected ${received} to be a number or null, but got ${typeof received}`,
|
|
pass: false,
|
|
};
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
const getServerDir = (workerIndex) => `../storage/${currentWorkerIndex}`;
|
|
const getTokensDir = () => `../storage/-1`;
|
|
let currentWorkerIndex = '-1';
|
|
|
|
test.beforeEach(async ({page}, testInfo) => {
|
|
currentWorkerIndex = testInfo.workerIndex.toString();
|
|
|
|
await fs.rm(getServerDir(), { recursive: true, force: true });
|
|
await fs.mkdir(getServerDir(), { recursive: true });
|
|
await fs.mkdir(getTokensDir(), { recursive: true });
|
|
|
|
await fs.writeFile(getTokensDir() + `/` + saltToken(currentWorkerIndex), currentWorkerIndex, 'utf8');
|
|
|
|
// Capture page console output and attach it to the Playwright report so
|
|
// failures show what the browser was logging. Each line ends up in the
|
|
// per-test "stdout" attachment, visible in the HTML report.
|
|
const logs = [];
|
|
page.on('console', (msg) => {
|
|
logs.push(`[${msg.type()}] ${msg.text()}`);
|
|
});
|
|
page.on('pageerror', (err) => {
|
|
logs.push(`[pageerror] ${err.message}`);
|
|
});
|
|
testInfo.__pageLogs = logs;
|
|
});
|
|
|
|
test.afterEach(async ({}, testInfo) => {
|
|
const logs = testInfo.__pageLogs;
|
|
if (!logs || logs.length === 0) return;
|
|
await testInfo.attach('page-console.log', {
|
|
body: logs.join('\n'),
|
|
contentType: 'text/plain',
|
|
});
|
|
});
|
|
|
|
async function setup(page) {
|
|
await page.addInitScript(() => {
|
|
localStorage.setItem('apiUrl', 'http://localhost:8080');
|
|
localStorage.setItem('lastServerOk', 1);
|
|
});
|
|
|
|
// Token is read from a cookie by the server; set it on the shared
|
|
// `localhost` domain so it's sent with both the app's (localhost:3000)
|
|
// and the API's (localhost:8080) requests.
|
|
await page.context().addCookies([{
|
|
name: 'token',
|
|
value: String(currentWorkerIndex),
|
|
domain: 'localhost',
|
|
path: '/',
|
|
}]);
|
|
|
|
await page.goto('/index.html');
|
|
|
|
await page.evaluate(()=> {
|
|
window.getTemporaryStorageDirHandle = async function() {
|
|
const root = await navigator.storage.getDirectory();
|
|
const subdir = await root.getDirectoryHandle('subdir', { create: true });
|
|
|
|
const files = [
|
|
{ name: 'README.md', content: 'Hello world' },
|
|
{ name: 'Notes.md', content: 'Some Text' }
|
|
];
|
|
|
|
for (const file of files) {
|
|
const fileHandle = await root.getFileHandle(file.name, { create: true });
|
|
const writable = await fileHandle.createWritable();
|
|
await writable.write(file.content);
|
|
await writable.close();
|
|
}
|
|
|
|
await root.getFileHandle('Chat.md', { create: true });
|
|
const fileHandle = await root.getFileHandle('config.json', { create: true });
|
|
const writable = await fileHandle.createWritable()
|
|
await writable.write('{}');
|
|
await writable.close();
|
|
|
|
return root;
|
|
};
|
|
})
|
|
|
|
await page.evaluate(() => {
|
|
init(document.getElementById('editor'));
|
|
});
|
|
|
|
await page.waitForSelector('#chat', {timeout: 10000});
|
|
await page.waitForSelector('#tree', {timeout: 5000});
|
|
}
|
|
|
|
test('sync new files from server', async ({ page }) => {
|
|
await createFileOnServer('File.md', 'test content');
|
|
await createFileOnServer('Another.md', '*italic*');
|
|
|
|
await setup(page);
|
|
|
|
// Check that existing files are not removed
|
|
await page.click(`#tree .tree-item:has-text('Notes')`);
|
|
await page.waitForTimeout(200);
|
|
expect(await page.evaluate(() => document.querySelector(".CodeMirror").CodeMirror.getValue())).toBe('# Notes\nSome Text');
|
|
await page.click(`#tree .tree-item:has-text('README')`);
|
|
await page.waitForTimeout(200);
|
|
expect(await page.evaluate(() => document.querySelector(".CodeMirror").CodeMirror.getValue())).toBe('# README\nHello world');
|
|
// Check that new files are added
|
|
await page.click(`#tree .tree-item:has-text('File')`);
|
|
await page.waitForTimeout(200);
|
|
expect(await page.evaluate(() => document.querySelector(".CodeMirror").CodeMirror.getValue())).toBe('# File\ntest content');
|
|
await page.click(`#tree .tree-item:has-text('Another')`);
|
|
await page.waitForTimeout(200);
|
|
expect(await page.evaluate(() => document.querySelector(".CodeMirror").CodeMirror.getValue())).toBe('# Another\n*italic*');
|
|
});
|
|
|
|
test('sync new files from client', async ({ page }) => {
|
|
await setup(page);
|
|
|
|
await page.click('#new-file');
|
|
await page.waitForTimeout(100);
|
|
await page.keyboard.type('Content');
|
|
await page.waitForTimeout(3000);
|
|
|
|
await expectFileOnServer(page, 'New file.md', 'Content');
|
|
});
|
|
|
|
test('sync new files from client, ignore current file in syncFiles', async ({ page }) => {
|
|
await setup(page);
|
|
|
|
const consoleMessages = [];
|
|
page.on('console', msg => {
|
|
consoleMessages.push({ type: msg.type(), text: msg.text() });
|
|
});
|
|
|
|
await page.click('#new-file');
|
|
await page.waitForTimeout(100);
|
|
await page.keyboard.type('Content');
|
|
|
|
// Two blurs: first primes server timestamps (syncFiles takes the
|
|
// "NEVER SYNCED BEFORE" path on the very first call), second is the
|
|
// one where the receive-side skip at files.js:229 actually fires —
|
|
// server echoes the current file and client should log the skip.
|
|
await page.evaluate(() => window.dispatchEvent(new Event('blur')));
|
|
await page.waitForTimeout(1500);
|
|
await page.evaluate(() => window.dispatchEvent(new Event('blur')));
|
|
await page.waitForTimeout(2000);
|
|
|
|
await expectFileOnServer(page, 'New file.md', 'Content');
|
|
|
|
// log() in app.js wraps messages with %c style directives, so match
|
|
// the substring rather than an exact text.
|
|
const skipped = consoleMessages.some(m =>
|
|
m.type === 'log' && m.text.includes('Skip receiving current file during bath sync /New file.md')
|
|
);
|
|
expect(skipped).toBe(true);
|
|
});
|
|
|
|
test('sync existing files from client', async ({ page }) => {
|
|
await createFileOnServer('File.md', 'test content');
|
|
await createFileOnServer('Another.md', '*italic*');
|
|
|
|
await setup(page);
|
|
|
|
// Check that existing files are not removed
|
|
await page.click(`#tree .tree-item:has-text('README')`);
|
|
await page.waitForTimeout(200);
|
|
expect(await page.evaluate(() => document.querySelector(".CodeMirror").CodeMirror.getValue())).toBe('# README\nHello world');
|
|
await page.click(`#tree .tree-item:has-text('Notes')`);
|
|
await page.waitForTimeout(200);
|
|
expect(await page.evaluate(() => document.querySelector(".CodeMirror").CodeMirror.getValue())).toBe('# Notes\nSome Text');
|
|
// Trigger syncFiles, first time to get server state
|
|
await page.evaluate(() => {
|
|
window.dispatchEvent(new Event('focus'));
|
|
});
|
|
|
|
await page.waitForTimeout(500);
|
|
|
|
// Trigger syncFiles, second time to send client files
|
|
await page.evaluate(() => {
|
|
window.dispatchEvent(new Event('focus'));
|
|
});
|
|
|
|
await page.waitForTimeout(500);
|
|
|
|
// Check that existing files from client are synced
|
|
await expectFileOnServer(page, 'README.md', 'Hello world');
|
|
await expectFileOnServer(page, 'Notes.md', 'Some Text');
|
|
|
|
// Check that existing server files are preserved
|
|
await page.click(`#tree .tree-item:has-text('file')`);
|
|
await page.waitForTimeout(200);
|
|
expect(await page.evaluate(() => document.querySelector(".CodeMirror").CodeMirror.getValue())).toBe('# File\ntest content');
|
|
await page.click(`#tree .tree-item:has-text('another')`);
|
|
await page.waitForTimeout(200);
|
|
expect(await page.evaluate(() => document.querySelector(".CodeMirror").CodeMirror.getValue())).toBe('# Another\n*italic*');
|
|
});
|
|
|
|
test('get changes for current file from server', async ({ page }) => {
|
|
await createFileOnServer('File.md', 'test content');
|
|
await createFileOnServer('Another.md', '*italic*');
|
|
|
|
await setup(page);
|
|
|
|
// Check that existing files are not removed
|
|
await page.click(`#tree .tree-item:has-text('file')`);
|
|
await page.waitForTimeout(200);
|
|
expect(await page.evaluate(() => document.querySelector(".CodeMirror").CodeMirror.getValue())).toBe('# File\ntest content');
|
|
await expectCurrentContent(page, '# File\ntest content');
|
|
|
|
await createFileOnServer('File.md', 'test content\nadded');
|
|
await createFileOnServer('File2.md', 'test content\nadded2');
|
|
await page.waitForTimeout(2000);
|
|
// await page.pause();
|
|
await expectCurrentContent(page, '# File\ntest content\nadded');
|
|
});
|
|
|
|
test('send changes from current file to server', async ({ page }) => {
|
|
await createFileOnServer('File.md', 'test content');
|
|
await createFileOnServer('Another.md', '*italic*');
|
|
|
|
await setup(page);
|
|
|
|
// Check that existing files are not removed
|
|
await page.click(`#tree .tree-item:has-text('file')`);
|
|
await page.waitForTimeout(200);
|
|
expect(await page.evaluate(() => document.querySelector(".CodeMirror").CodeMirror.getValue())).toBe('# File\ntest content');
|
|
await expectCurrentContent(page, '# File\ntest content');
|
|
|
|
// Update file on server, check that changes received
|
|
await createFileOnServer('File.md', 'test content\nadded');
|
|
await page.waitForTimeout(2000);
|
|
await expectCurrentContent(page, '# File\ntest content\nadded');
|
|
|
|
// Place cursor at the end
|
|
await page.keyboard.press('Meta+ArrowDown');
|
|
await page.keyboard.press('Enter');
|
|
|
|
await page.keyboard.type('added from client');
|
|
await page.waitForTimeout(2000);
|
|
await expectFileOnServer(page, 'file.md', 'test content\nadded\nadded from client');
|
|
});
|
|
|
|
test('changed on both client and serve, should merge', async ({ page }) => {
|
|
await createFileOnServer('File.md', 'test content');
|
|
|
|
await setup(page);
|
|
|
|
await page.click(`#tree .tree-item:has-text('file')`);
|
|
|
|
await page.waitForTimeout(200);
|
|
|
|
expect(await page.evaluate(() => document.querySelector(".CodeMirror").CodeMirror.getValue())).toBe('# File\ntest content');
|
|
// Disable sync by intercepting every API call and aborting it. Cleaner
|
|
// than clearing the auth cookie — no "Wrong token" noise on the server,
|
|
// and the client just gets a network error (which syncLocalFileWithServer
|
|
// already handles silently).
|
|
const API_HOST = 'localhost:8080';
|
|
await page.route(`http://${API_HOST}/**`, (route) => route.abort());
|
|
|
|
// Modify on server
|
|
await createFileOnServer('File.md', 'test content\nadded from server');
|
|
await page.waitForTimeout(2000);
|
|
|
|
// Modify on client
|
|
await page.keyboard.press('Meta+ArrowDown');
|
|
await page.keyboard.press('Enter');
|
|
await page.keyboard.type('addded from client');
|
|
|
|
// Enable sync
|
|
await page.unroute(`http://${API_HOST}/**`);
|
|
|
|
await page.waitForTimeout(2000);
|
|
await expectFileOnServer(page, 'File.md', 'test content\nadded from server\naddded from client');
|
|
await expectCurrentContent(page, '# File\ntest content\nadded from server\naddded from client');
|
|
});
|
|
|
|
test("sync one new file from client doesn't conflict with syncFiles", async ({ page }) => {
|
|
await setup(page);
|
|
|
|
await page.click('#new-file');
|
|
await page.waitForTimeout(100);
|
|
await page.keyboard.type('abc');
|
|
await page.keyboard.type('def');
|
|
await page.keyboard.type('abc');
|
|
|
|
await page.waitForTimeout(500);
|
|
|
|
// Trigger syncFiles
|
|
await page.evaluate(() => {
|
|
window.dispatchEvent(new Event('focus'));
|
|
});
|
|
|
|
await page.keyboard.type('def');
|
|
|
|
await page.keyboard.press('Enter');
|
|
await page.keyboard.type('def');
|
|
await page.keyboard.press('Enter');
|
|
await page.keyboard.type('Content');
|
|
await page.waitForTimeout(3000);
|
|
|
|
await expectFileOnServer(page, 'New file.md', 'abcdefabcdef\ndef\nContent');
|
|
});
|
|
|
|
test('delete files on client will propagate to server as well', async ({ page }) => {
|
|
await createFileOnServer('File.md', 'test content');
|
|
await createFileOnServer('Another.md', '*italic*');
|
|
|
|
await setup(page);
|
|
|
|
await page.waitForTimeout(300);
|
|
|
|
await page.click(`#tree .tree-item:has-text('Notes')`);
|
|
|
|
await page.waitForTimeout(200);
|
|
|
|
expect(await page.evaluate(() => document.querySelector(".CodeMirror").CodeMirror.getValue())).toBe('# Notes\nSome Text');
|
|
await page.click(`#tree .tree-item:has-text('README')`);
|
|
await page.waitForTimeout(200);
|
|
expect(await page.evaluate(() => document.querySelector(".CodeMirror").CodeMirror.getValue())).toBe('# README\nHello world');
|
|
await page.click(`#tree .tree-item:has-text('file')`);
|
|
|
|
await page.waitForTimeout(200);
|
|
|
|
expect(await page.evaluate(() => document.querySelector(".CodeMirror").CodeMirror.getValue())).toBe('# File\ntest content');
|
|
await page.click(`#tree .tree-item:has-text('another')`);
|
|
await page.waitForTimeout(200);
|
|
expect(await page.evaluate(() => document.querySelector(".CodeMirror").CodeMirror.getValue())).toBe('# Another\n*italic*');
|
|
await page.click(`#tree .tree-item:has-text('another')`);
|
|
|
|
await page.waitForTimeout(200);
|
|
|
|
expect(await page.evaluate(() => document.querySelector(".CodeMirror").CodeMirror.getValue())).toBe('# Another\n*italic*');
|
|
await page.keyboard.press('Meta+d');
|
|
|
|
await page.waitForTimeout(1000);
|
|
|
|
// syncFiles should propagate deletion to server
|
|
await page.evaluate(() => {
|
|
window.dispatchEvent(new Event('focus'));
|
|
});
|
|
|
|
await page.waitForTimeout(500);
|
|
|
|
// await page.pause();
|
|
|
|
expectFileOnServer(page, 'File.md', 'test content');
|
|
expectNoFileOnServer(page, 'another.md');
|
|
});
|
|
|
|
test('files exist on both client and server, config is not removed on first sync', async ({ page }) => {
|
|
await createFileOnServer('File.md', 'test content');
|
|
await createFileOnServer('Another.md', '*italic*');
|
|
|
|
await setup(page);
|
|
await page.waitForTimeout(300);
|
|
|
|
// Check that existing files are not removed
|
|
await page.click(`#tree .tree-item:has-text('Notes')`);
|
|
await page.waitForTimeout(200);
|
|
expect(await page.evaluate(() => document.querySelector(".CodeMirror").CodeMirror.getValue())).toBe('# Notes\nSome Text');
|
|
await page.click(`#tree .tree-item:has-text('README')`);
|
|
await page.waitForTimeout(200);
|
|
expect(await page.evaluate(() => document.querySelector(".CodeMirror").CodeMirror.getValue())).toBe('# README\nHello world');
|
|
// Check that new files are added
|
|
await page.click(`#tree .tree-item:has-text('file')`);
|
|
await page.waitForTimeout(200);
|
|
expect(await page.evaluate(() => document.querySelector(".CodeMirror").CodeMirror.getValue())).toBe('# File\ntest content');
|
|
await page.click(`#tree .tree-item:has-text('another')`);
|
|
await page.waitForTimeout(200);
|
|
expect(await page.evaluate(() => document.querySelector(".CodeMirror").CodeMirror.getValue())).toBe('# Another\n*italic*');
|
|
// Trigger syncFiles
|
|
await page.waitForTimeout(300);
|
|
await page.evaluate(() => {
|
|
window.dispatchEvent(new Event('focus'));
|
|
});
|
|
await page.waitForTimeout(300);
|
|
|
|
await expectFileOnServer(page, 'File.md', 'test content');
|
|
await expectFileOnServer(page, 'Another.md', '*italic*');
|
|
|
|
const configExists = await fs.access(path.join(getServerDir(), 'config.json')).then(() => true).catch(() => false);
|
|
expect(configExists).toBe(true);
|
|
});
|
|
|
|
test('files exist on both client and server, serverFiles contains proper server files', async ({ page }) => {
|
|
await createFileOnServer('File.md', 'test content');
|
|
await createFileOnServer('dir/File2.md', 'test content2');
|
|
await createFileOnServer('Another.md', '*italic*');
|
|
|
|
await setup(page);
|
|
await page.waitForTimeout(300);
|
|
|
|
// Check that existing files are not removed
|
|
await page.click(`#tree .tree-item:has-text('Notes')`);
|
|
await page.waitForTimeout(200);
|
|
expect(await page.evaluate(() => document.querySelector(".CodeMirror").CodeMirror.getValue())).toBe('# Notes\nSome Text');
|
|
await page.click(`#tree .tree-item:has-text('README')`);
|
|
await page.waitForTimeout(200);
|
|
expect(await page.evaluate(() => document.querySelector(".CodeMirror").CodeMirror.getValue())).toBe('# README\nHello world');
|
|
// Check that new files are added
|
|
await page.click(`#tree .tree-item:has-text('File')`);
|
|
await page.waitForTimeout(200);
|
|
expect(await page.evaluate(() => document.querySelector(".CodeMirror").CodeMirror.getValue())).toBe('# File\ntest content');
|
|
await page.click(`#tree .tree-item:has-text('Another')`);
|
|
await page.waitForTimeout(200);
|
|
expect(await page.evaluate(() => document.querySelector(".CodeMirror").CodeMirror.getValue())).toBe('# Another\n*italic*');
|
|
// Trigger syncFiles
|
|
await page.evaluate(() => {
|
|
window.dispatchEvent(new Event('focus'));
|
|
});
|
|
await page.waitForTimeout(300);
|
|
|
|
await expectFileOnServer(page, 'File.md', 'test content');
|
|
await expectFileOnServer(page, 'Another.md', '*italic*');
|
|
|
|
let filesOnServer = await page.evaluate(() => {
|
|
return server['files'];
|
|
});
|
|
expect(filesOnServer).toEqual({
|
|
'Chat.md': {
|
|
hash: expect.any(Number),
|
|
isFile: true,
|
|
lastModified: expect.any(Number),
|
|
lastClientModified: expect.any(Number),
|
|
path: '/Chat.md'
|
|
},
|
|
'Notes.md': {
|
|
hash: expect.any(Number),
|
|
isFile: true,
|
|
lastModified: expect.any(Number),
|
|
lastClientModified: expect.any(Number),
|
|
path: '/Notes.md'
|
|
},
|
|
'README.md': {
|
|
hash: expect.any(Number),
|
|
isFile: true,
|
|
lastModified: expect.any(Number),
|
|
lastClientModified: expect.any(Number),
|
|
path: '/README.md'
|
|
},
|
|
'Another.md': {
|
|
hash: expect.any(Number),
|
|
isFile: true,
|
|
lastModified: expect.toBeNumberOrNull(),
|
|
lastClientModified: expect.any(Number),
|
|
path: '/Another.md'
|
|
},
|
|
'config.json': {
|
|
hash: expect.any(Number),
|
|
isFile: true,
|
|
lastModified: expect.any(Number),
|
|
lastClientModified: expect.any(Number),
|
|
path: '/config.json'
|
|
},
|
|
'dir/': {
|
|
'File2.md': {
|
|
hash: expect.any(Number),
|
|
isFile: true,
|
|
lastModified: expect.any(Number),
|
|
lastClientModified: expect.any(Number),
|
|
path: '/dir/File2.md'
|
|
}
|
|
},
|
|
'File.md': {
|
|
isFile: true,
|
|
hash: expect.any(Number),
|
|
lastModified: expect.any(Number),
|
|
lastClientModified: expect.any(Number),
|
|
path: '/File.md'
|
|
},
|
|
'happiness/': {
|
|
'Boredom is just an emotion.md': {
|
|
isFile: true,
|
|
hash: expect.any(Number),
|
|
lastModified: expect.any(Number),
|
|
lastClientModified: expect.any(Number),
|
|
path: '/happiness/Boredom is just an emotion.md'
|
|
},
|
|
'Abundant meditation.md': {
|
|
isFile: true,
|
|
hash: expect.any(Number),
|
|
lastModified: expect.any(Number),
|
|
lastClientModified: expect.any(Number),
|
|
path: '/happiness/Abundant meditation.md'
|
|
}
|
|
},
|
|
'🪴 Welcome.md': {
|
|
isFile: true,
|
|
hash: expect.any(Number),
|
|
lastModified: expect.any(Number),
|
|
lastClientModified: expect.any(Number),
|
|
path: '/🪴 Welcome.md'
|
|
},
|
|
'Hotkeys.md': {
|
|
isFile: true,
|
|
hash: expect.any(Number),
|
|
lastModified: expect.any(Number),
|
|
lastClientModified: expect.any(Number),
|
|
path: '/Hotkeys.md'
|
|
},
|
|
'Links.md': {
|
|
isFile: true,
|
|
hash: expect.any(Number),
|
|
lastModified: expect.any(Number),
|
|
lastClientModified: expect.any(Number),
|
|
path: '/Links.md'
|
|
},
|
|
'My Project.md': {
|
|
isFile: true,
|
|
hash: expect.any(Number),
|
|
lastModified: expect.any(Number),
|
|
lastClientModified: expect.any(Number),
|
|
path: '/My Project.md'
|
|
},
|
|
'Markdown Guide.md': {
|
|
isFile: true,
|
|
hash: expect.any(Number),
|
|
lastModified: expect.any(Number),
|
|
lastClientModified: expect.any(Number),
|
|
path: '/Markdown Guide.md'
|
|
},
|
|
'brain/': {
|
|
'Change your environment instead of using willpower.md': {
|
|
isFile: true,
|
|
hash: expect.any(Number),
|
|
lastModified: expect.any(Number),
|
|
lastClientModified: expect.any(Number),
|
|
path: '/brain/Change your environment instead of using willpower.md'
|
|
},
|
|
'Brain is the most complex object in known universe.md': {
|
|
isFile: true,
|
|
hash: expect.any(Number),
|
|
lastModified: expect.any(Number),
|
|
lastClientModified: expect.any(Number),
|
|
path: '/brain/Brain is the most complex object in known universe.md'
|
|
},
|
|
'We think that we understand, but in reality we just know.md': {
|
|
isFile: true,
|
|
hash: expect.any(Number),
|
|
lastModified: expect.any(Number),
|
|
lastClientModified: expect.any(Number),
|
|
path: '/brain/We think that we understand, but in reality we just know.md'
|
|
},
|
|
'Zettelkasten.md': {
|
|
isFile: true,
|
|
hash: expect.any(Number),
|
|
lastModified: expect.any(Number),
|
|
lastClientModified: expect.any(Number),
|
|
path: '/brain/Zettelkasten.md'
|
|
}
|
|
},
|
|
});
|
|
|
|
// await page.pause();
|
|
if (!await page.locator(`#tree .tree-item:text-is('dir')`).evaluate(el => el.classList.contains("expanded"))) {
|
|
await page.click(`#tree .tree-item:text-is('dir')`);
|
|
await page.waitForTimeout(100);
|
|
}
|
|
await page.click(`#tree .tree-item:has-text('file2')`);
|
|
await page.waitForTimeout(200);
|
|
expect(await page.evaluate(() => document.querySelector(".CodeMirror").CodeMirror.getValue())).toBe('# File2\ntest content2');
|
|
});
|
|
|
|
|
|
test('sync changes from client, update clientLastModified & lastClientModified', async ({ page }) => {
|
|
await setup(page);
|
|
|
|
await page.click('#new-file');
|
|
await page.waitForTimeout(100);
|
|
await page.keyboard.type('Content');
|
|
await page.waitForTimeout(3000);
|
|
|
|
await expectFileOnServer(page, 'New file.md', 'Content');
|
|
|
|
let serverFiles = await page.evaluate(() => {
|
|
return server['files'];
|
|
});
|
|
|
|
let clientFileLastModified = await page.evaluate(async () => {
|
|
let fileHandle = await getFileHandle('/New file.md');
|
|
let file = await fileHandle.getFile();
|
|
|
|
return file.lastModified;
|
|
});
|
|
|
|
expect(serverFiles['New file.md'].lastModified).not.toBeNull();
|
|
expect(serverFiles['New file.md'].lastClientModified).toEqual(clientFileLastModified);
|
|
});
|
|
|
|
async function createFileOnServer(filepath, content) {
|
|
const p = path.join(getServerDir(), filepath);
|
|
|
|
// Create all intermediate directories
|
|
await fs.mkdir(path.dirname(p), { recursive: true });
|
|
|
|
await fs.writeFile(p, content, 'utf8');
|
|
}
|
|
|
|
async function expectFileOnServer(page, filepath, expectedContent) {
|
|
const p = path.join(getServerDir(), filepath);
|
|
const actualContent = await fs.readFile(p, 'utf8');
|
|
|
|
expect(actualContent).toBe(expectedContent);
|
|
}
|
|
|
|
async function expectNoFileOnServer(page, filepath) {
|
|
const p = path.join(getServerDir(), filepath);
|
|
|
|
const exists = await fs.access(p).then(() => true).catch(() => false);
|
|
expect(exists).toBe(false);
|
|
}
|
|
|
|
function saltToken(token, salt = '') {
|
|
return crypto.createHash('sha256')
|
|
.update(token + salt)
|
|
.digest('hex');
|
|
}
|
|
|
|
|
|
async function expectCurrentContent(page, content) {
|
|
const codeMirrorContent = await page.evaluate(() => {
|
|
const cm = document.querySelector('.CodeMirror').CodeMirror;
|
|
return cm.getValue();
|
|
});
|
|
expect(codeMirrorContent).toBe(content);
|
|
}
|
|
|