397 lines
17 KiB
JavaScript
397 lines
17 KiB
JavaScript
const {test, expect} = require('@playwright/test');
|
|
|
|
test.beforeEach(async ({page}) => {
|
|
await page.goto('/index.html');
|
|
|
|
await page.waitForSelector('#tree', {timeout: 5000});
|
|
});
|
|
|
|
test('send message to chat', async ({ page }) => {
|
|
await page.click(`#tree .tree-item:has-text('chat')`);
|
|
await page.waitForSelector('#chat');
|
|
await page.keyboard.type('My message');
|
|
await page.waitForTimeout(300);
|
|
// TODO I believe chat is reloaded 2 times for some reason, it blinks, and thus removes previous message
|
|
// Or wait for timeout before typing message doesn't help hmm
|
|
await page.keyboard.press('Enter');
|
|
|
|
await page.waitForSelector('.message');
|
|
let content = await page.textContent('.message-content')
|
|
expect(content).toBe('My message');
|
|
|
|
});
|
|
|
|
test('select all in chat input selects input text, not bubbles', async ({page}) => {
|
|
await page.click(`#tree .tree-item:has-text('chat')`);
|
|
await page.waitForSelector('#chat');
|
|
await page.keyboard.type('First message');
|
|
await page.waitForTimeout(300);
|
|
await page.keyboard.press('Enter');
|
|
await page.waitForSelector('.message');
|
|
|
|
await page.locator('#chat-input').click();
|
|
await page.keyboard.type('to be cleared');
|
|
await expect(page.locator('#chat-input')).toHaveValue('to be cleared');
|
|
|
|
const modifier = process.platform === 'darwin' ? 'Meta' : 'Control';
|
|
await page.keyboard.press(`${modifier}+a`);
|
|
await page.keyboard.press('Delete');
|
|
|
|
await expect(page.locator('#chat-input')).toHaveValue('');
|
|
await expect(page.locator('.message-content')).toHaveText('First message');
|
|
});
|
|
|
|
test('move to dir creates a new file inside that dir', async ({page}) => {
|
|
await page.evaluate(() => {
|
|
window.getTemporaryStorageDirHandle = async function () {
|
|
const root = await navigator.storage.getDirectory();
|
|
await root.getDirectoryHandle('projects', {create: true});
|
|
return root;
|
|
};
|
|
});
|
|
await page.evaluate(() => init(document.getElementById('editor')));
|
|
|
|
await page.click(`#tree .tree-item:has-text('chat')`);
|
|
await page.waitForSelector('#chat');
|
|
await page.keyboard.type('MyTask');
|
|
await page.waitForTimeout(300);
|
|
await page.keyboard.press('Enter');
|
|
await page.waitForSelector('.message');
|
|
|
|
await page.hover('.message');
|
|
await page.locator('.to-file-btn').first().click({force: true});
|
|
await page.waitForSelector('#search', {state: 'visible'});
|
|
|
|
await page.locator('#search-results li[data-dir="projects"]').click();
|
|
await page.waitForSelector('.message', {state: 'detached'});
|
|
|
|
const exists = await page.evaluate(async () => {
|
|
const root = await navigator.storage.getDirectory();
|
|
const projects = await root.getDirectoryHandle('projects');
|
|
try { await projects.getFileHandle('MyTask.md'); return true; }
|
|
catch { return false; }
|
|
});
|
|
expect(exists).toBe(true);
|
|
});
|
|
|
|
test('move to root creates a new file at root', async ({page}) => {
|
|
await page.evaluate(() => {
|
|
window.getTemporaryStorageDirHandle = async function () {
|
|
return await navigator.storage.getDirectory();
|
|
};
|
|
});
|
|
await page.evaluate(() => init(document.getElementById('editor')));
|
|
|
|
await page.click(`#tree .tree-item:has-text('chat')`);
|
|
await page.waitForSelector('#chat');
|
|
await page.keyboard.type('RootMsg');
|
|
await page.waitForTimeout(300);
|
|
await page.keyboard.press('Enter');
|
|
await page.waitForSelector('.message');
|
|
|
|
await page.hover('.message');
|
|
await page.locator('.to-file-btn').first().click({force: true});
|
|
await page.waitForSelector('#search', {state: 'visible'});
|
|
|
|
await page.locator('#search-results li[data-dir=""]').click();
|
|
await page.waitForSelector('.message', {state: 'detached'});
|
|
|
|
const exists = await page.evaluate(async () => {
|
|
const root = await navigator.storage.getDirectory();
|
|
try { await root.getFileHandle('RootMsg.md'); return true; }
|
|
catch { return false; }
|
|
});
|
|
expect(exists).toBe(true);
|
|
});
|
|
|
|
test('move to existing file appends content', async ({page}) => {
|
|
// Seed once, then return the same root on subsequent calls — otherwise the
|
|
// app's repeated getRootDirHandle() calls would re-overwrite Notes.md.
|
|
await page.evaluate(async () => {
|
|
const root = await navigator.storage.getDirectory();
|
|
const fh = await root.getFileHandle('Notes.md', {create: true});
|
|
const w = await fh.createWritable();
|
|
await w.write('# Notes');
|
|
await w.close();
|
|
window.getTemporaryStorageDirHandle = async () => navigator.storage.getDirectory();
|
|
});
|
|
await page.evaluate(() => init(document.getElementById('editor')));
|
|
|
|
await page.click(`#tree .tree-item:has-text('chat')`);
|
|
await page.waitForSelector('#chat');
|
|
await page.keyboard.type('Append me');
|
|
await page.waitForTimeout(300);
|
|
await page.keyboard.press('Enter');
|
|
await page.waitForSelector('.message');
|
|
|
|
await page.hover('.message');
|
|
await page.locator('.to-file-btn').first().click({force: true});
|
|
await page.waitForSelector('#search', {state: 'visible'});
|
|
|
|
await page.locator('#search-results li[data-path="/Notes.md"]').click();
|
|
await page.waitForSelector('.message', {state: 'detached'});
|
|
|
|
await page.click(`#tree .tree-item:has-text('Notes')`);
|
|
await page.waitForTimeout(200);
|
|
const content = await page.evaluate(() => document.querySelector('.CodeMirror').CodeMirror.getValue());
|
|
expect(content).toContain('# Notes');
|
|
expect(content).toContain('Append me');
|
|
});
|
|
|
|
test('move to file does not prepend a timestamp', async ({page}) => {
|
|
await page.evaluate(async () => {
|
|
const root = await navigator.storage.getDirectory();
|
|
const fh = await root.getFileHandle('Notes.md', {create: true});
|
|
const w = await fh.createWritable();
|
|
await w.write('# Notes');
|
|
await w.close();
|
|
window.getTemporaryStorageDirHandle = async () => navigator.storage.getDirectory();
|
|
});
|
|
await page.evaluate(() => init(document.getElementById('editor')));
|
|
|
|
await page.click(`#tree .tree-item:has-text('chat')`);
|
|
await page.waitForSelector('#chat');
|
|
await page.keyboard.type('Attention is all you need');
|
|
await page.waitForTimeout(300);
|
|
await page.keyboard.press('Enter');
|
|
await page.waitForSelector('.message');
|
|
|
|
await page.hover('.message');
|
|
await page.locator('.to-file-btn').first().click({force: true});
|
|
await page.waitForSelector('#search', {state: 'visible'});
|
|
|
|
await page.locator('#search-results li[data-path="/Notes.md"]').click();
|
|
await page.waitForSelector('.message', {state: 'detached'});
|
|
|
|
await page.click(`#tree .tree-item:has-text('Notes')`);
|
|
await page.waitForTimeout(200);
|
|
const content = await page.evaluate(() => document.querySelector('.CodeMirror').CodeMirror.getValue());
|
|
expect(content).toContain('Attention is all you need');
|
|
// The body must not be prefixed with `HH:MM` - that's reserved for the
|
|
// chat→journal flow, not move-to-file (web/lib/md.js:addHeaderAndText).
|
|
expect(content).not.toMatch(/`\d{2}:\d{2}`\s*500k/);
|
|
});
|
|
|
|
test('move to recent file does not prepend a timestamp', async ({page}) => {
|
|
await page.evaluate(() => {
|
|
window.getTemporaryStorageDirHandle = async function () {
|
|
const root = await navigator.storage.getDirectory();
|
|
await root.getFileHandle('File.md', {create: true});
|
|
return root;
|
|
};
|
|
});
|
|
await page.evaluate(() => init(document.getElementById('editor')));
|
|
|
|
await page.click(`#tree .tree-item:has-text('chat')`);
|
|
await page.waitForSelector('#chat');
|
|
await page.keyboard.type('Attention is all you need');
|
|
await page.waitForTimeout(300);
|
|
await page.keyboard.press('Enter');
|
|
await page.waitForSelector('.message');
|
|
|
|
await page.hover('.message');
|
|
await page.locator('.action-btn').filter({hasText: 'File'}).click({force: true});
|
|
await page.waitForSelector('.message', {state: 'detached'});
|
|
|
|
await page.click(`#tree .tree-item:has-text('File')`);
|
|
await page.waitForTimeout(200);
|
|
const fileContent = await page.evaluate(() =>
|
|
document.querySelector('.CodeMirror').CodeMirror.getValue());
|
|
expect(fileContent).toContain('Attention is all you need');
|
|
expect(fileContent).not.toMatch(/`\d{2}:\d{2}`\s*500k/);
|
|
});
|
|
|
|
test('system dirs (archive, today) are hidden in move-to-file modal', async ({page}) => {
|
|
await page.evaluate(() => {
|
|
window.getTemporaryStorageDirHandle = async function () {
|
|
const root = await navigator.storage.getDirectory();
|
|
await root.getDirectoryHandle('archive', {create: true});
|
|
await root.getDirectoryHandle('projects', {create: true});
|
|
return root;
|
|
};
|
|
});
|
|
await page.evaluate(() => init(document.getElementById('editor')));
|
|
|
|
await page.click(`#tree .tree-item:has-text('chat')`);
|
|
await page.waitForSelector('#chat');
|
|
await page.keyboard.type('Hello');
|
|
await page.waitForTimeout(300);
|
|
await page.keyboard.press('Enter');
|
|
await page.waitForSelector('.message');
|
|
|
|
await page.hover('.message');
|
|
await page.locator('.to-file-btn').first().click({force: true});
|
|
await page.waitForSelector('#search', {state: 'visible'});
|
|
|
|
await expect(page.locator('#search-results li[data-dir="projects"]')).toBeVisible();
|
|
await expect(page.locator('#search-results li[data-dir="archive"]')).toHaveCount(0);
|
|
await expect(page.locator('#search-results li[data-dir="today"]')).toHaveCount(0);
|
|
});
|
|
|
|
test('send to chat and move to recent file', async ({ page }) => {
|
|
await page.evaluate(() => {
|
|
window.getTemporaryStorageDirHandle = async function() {
|
|
const root = await navigator.storage.getDirectory();
|
|
const fileHandle = await root.getFileHandle('File.md', { create: true });
|
|
|
|
return root;
|
|
};
|
|
});
|
|
|
|
await page.evaluate(() => {
|
|
init(document.getElementById("editor"));
|
|
});
|
|
|
|
await page.click(`#tree .tree-item:has-text('chat')`);
|
|
await page.waitForSelector('#chat');
|
|
await page.keyboard.type('My message');
|
|
await page.waitForTimeout(300);
|
|
await page.keyboard.press('Enter');
|
|
|
|
await page.waitForSelector('.message');
|
|
let content = await page.textContent('.message-content')
|
|
expect(content).toBe('My message');
|
|
|
|
await page.hover('.message');
|
|
await page.locator('.action-btn').filter({hasText: 'File'}).click({force: true});
|
|
await page.waitForSelector('.message', {state: 'detached'});
|
|
|
|
await page.click(`#tree .tree-item:has-text('File')`);
|
|
await page.waitForTimeout(200);
|
|
const fileContent = await page.evaluate(() =>
|
|
document.querySelector('.CodeMirror').CodeMirror.getValue());
|
|
expect(fileContent).toContain('# File');
|
|
expect(fileContent).toContain('My message');
|
|
});
|
|
|
|
// Regression: moving a lowercase-starting chat message via the to-file-btn
|
|
// used to crash because chat.js applied ucfirst() to the text before passing
|
|
// it to the search modal, while the DOM dataset.text stayed in original case.
|
|
// `find(el => el.dataset.text === selectedMsgText)` then returned undefined
|
|
// and modals.js threw "Cannot read properties of undefined (reading 'classList')".
|
|
test('move-to-file works for messages that start with a lowercase letter', async ({page}) => {
|
|
await page.evaluate(async () => {
|
|
const root = await navigator.storage.getDirectory();
|
|
const fh = await root.getFileHandle('Notes.md', {create: true});
|
|
const w = await fh.createWritable();
|
|
await w.write('# Notes');
|
|
await w.close();
|
|
window.getTemporaryStorageDirHandle = async () => navigator.storage.getDirectory();
|
|
});
|
|
await page.evaluate(() => init(document.getElementById('editor')));
|
|
|
|
await page.click(`#tree .tree-item:has-text('chat')`);
|
|
await page.waitForSelector('#chat');
|
|
// Lowercase starting letter is the trigger for the original bug.
|
|
await page.keyboard.type('lowercase start');
|
|
await page.waitForTimeout(300);
|
|
await page.keyboard.press('Enter');
|
|
await page.waitForSelector('.message');
|
|
|
|
const errors = [];
|
|
page.on('pageerror', err => errors.push(err.message));
|
|
|
|
await page.hover('.message');
|
|
await page.locator('.to-file-btn').first().click({force: true});
|
|
await page.waitForSelector('#search', {state: 'visible'});
|
|
|
|
await page.locator('#search-results li[data-path="/Notes.md"]').click();
|
|
await page.waitForSelector('.message', {state: 'detached'});
|
|
|
|
// No uncaught exceptions should have been raised during the flow.
|
|
expect(errors).toEqual([]);
|
|
|
|
await page.click(`#tree .tree-item:has-text('Notes')`);
|
|
await page.waitForTimeout(200);
|
|
const content = await page.evaluate(() =>
|
|
document.querySelector('.CodeMirror').CodeMirror.getValue());
|
|
// The text written to the file is capitalised (ucfirst applied at the
|
|
// write step, AFTER the DOM lookup, so it doesn't break find()).
|
|
expect(content).toContain('Lowercase start');
|
|
});
|
|
|
|
// Regression: a chat message containing `"` used to crash to-file because
|
|
// escapeHtml() left quotes unescaped, the `data-text="..."` attribute closed
|
|
// early at the first `"`, and the modal's `dataset.text === selectedMsgText`
|
|
// lookup returned undefined.
|
|
test('move-to-file works for messages containing double quotes', async ({page}) => {
|
|
await page.evaluate(async () => {
|
|
const root = await navigator.storage.getDirectory();
|
|
const fh = await root.getFileHandle('Notes.md', {create: true});
|
|
const w = await fh.createWritable();
|
|
await w.write('# Notes');
|
|
await w.close();
|
|
window.getTemporaryStorageDirHandle = async () => navigator.storage.getDirectory();
|
|
});
|
|
await page.evaluate(() => init(document.getElementById('editor')));
|
|
|
|
await page.click(`#tree .tree-item:has-text('chat')`);
|
|
await page.waitForSelector('#chat');
|
|
const quoted = 'catches "file changed in vim." Without it';
|
|
await page.keyboard.type(quoted);
|
|
await page.waitForTimeout(300);
|
|
await page.keyboard.press('Enter');
|
|
await page.waitForSelector('.message');
|
|
|
|
const errors = [];
|
|
page.on('pageerror', err => errors.push(err.message));
|
|
|
|
await page.hover('.message');
|
|
await page.locator('.to-file-btn').first().click({force: true});
|
|
await page.waitForSelector('#search', {state: 'visible'});
|
|
|
|
await page.locator('#search-results li[data-path="/Notes.md"]').click();
|
|
await page.waitForSelector('.message', {state: 'detached'});
|
|
|
|
expect(errors).toEqual([]);
|
|
|
|
await page.click(`#tree .tree-item:has-text('Notes')`);
|
|
await page.waitForTimeout(200);
|
|
const content = await page.evaluate(() =>
|
|
document.querySelector('.CodeMirror').CodeMirror.getValue());
|
|
// ucfirst capitalises the first letter at the write step (see the
|
|
// lowercase-letter regression test above). What matters here is that
|
|
// the embedded quotes round-trip intact.
|
|
expect(content).toContain('Catches "file changed in vim." Without it');
|
|
});
|
|
|
|
// Regression: clicking the complete checkbox on a message containing `"` used
|
|
// to be a no-op - escapeHtml() left the quote unescaped in
|
|
// `data-text="..."`, so el.dataset.text truncated at the first `"`, the
|
|
// regex in toggleChatMessage didn't match anything, and the on-disk `- [ ]`
|
|
// stayed `- [ ]`.
|
|
test('complete-btn toggles a message containing double quotes', async ({page}) => {
|
|
await page.evaluate(() => {
|
|
window.getTemporaryStorageDirHandle = async () => navigator.storage.getDirectory();
|
|
});
|
|
await page.evaluate(() => init(document.getElementById('editor')));
|
|
|
|
await page.click(`#tree .tree-item:has-text('chat')`);
|
|
await page.waitForSelector('#chat');
|
|
const quoted = 'ask about "uuid": "019e4eea-32b1-7c08-a000-3a1ecd0a6c07"';
|
|
await page.keyboard.type(quoted);
|
|
await page.waitForTimeout(300);
|
|
await page.keyboard.press('Enter');
|
|
await page.waitForSelector('.message');
|
|
|
|
const errors = [];
|
|
page.on('pageerror', err => errors.push(err.message));
|
|
|
|
await page.hover('.message');
|
|
await page.locator('.message .complete-btn').first().click({force: true});
|
|
await expect(page.locator('.message.completed')).toHaveCount(1);
|
|
|
|
// Reread Chat.md from disk - the toggle should have rewritten the line
|
|
// from `- [ ]` to `- [x]`. Before the fix, the regex match in
|
|
// toggleChatMessage failed (dataset.text was truncated at the first `"`)
|
|
// and the file was left untouched.
|
|
await expect.poll(async () => {
|
|
return await page.evaluate(async () => {
|
|
const root = await navigator.storage.getDirectory();
|
|
const fh = await root.getFileHandle('Chat.md');
|
|
return (await fh.getFile()).text();
|
|
});
|
|
}).toMatch(/^- \[x\] `\d{2}:\d{2}` ask about "uuid": "019e4eea-32b1-7c08-a000-3a1ecd0a6c07"\s*$/m);
|
|
expect(errors).toEqual([]);
|
|
});
|