feat: Initial clean release of CHM
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
这个提交包含在:
@@ -0,0 +1,247 @@
|
||||
import { test, expect } from './util/test';
|
||||
import MainScreen from './models/MainScreen';
|
||||
import { _electron as electron } from '@playwright/test';
|
||||
import { writeFile } from 'fs-extra';
|
||||
import { join } from 'path';
|
||||
import createStartupArgs from './util/createStartupArgs';
|
||||
import getMainWindow from './util/getMainWindow';
|
||||
import setFilePickerResponse from './util/setFilePickerResponse';
|
||||
import setMessageBoxResponse from './util/setMessageBoxResponse';
|
||||
import getImageSourceSize from './util/getImageSourceSize';
|
||||
import setSettingValue from './util/setSettingValue';
|
||||
|
||||
|
||||
test.describe('main', () => {
|
||||
test('app should launch', async ({ mainWindow }) => {
|
||||
// A window should open with the correct title
|
||||
expect(await mainWindow.title()).toMatch(/^Joplin/);
|
||||
|
||||
const mainPage = await new MainScreen(mainWindow).setup();
|
||||
await mainPage.waitFor();
|
||||
});
|
||||
|
||||
test('app should support French localization', async ({ mainWindow, electronApp }) => {
|
||||
await setSettingValue(electronApp, mainWindow, 'locale', 'fr_FR');
|
||||
// The "Notebooks" header should be localized
|
||||
const localizedText = mainWindow.getByText('Carnets').first();
|
||||
await expect(localizedText).toBeAttached();
|
||||
});
|
||||
|
||||
test('should be able to create and edit a new note', async ({ mainWindow }) => {
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
const editor = await mainScreen.createNewNote('Test note');
|
||||
|
||||
// Note list should contain the new note
|
||||
await expect(mainScreen.noteList.getNoteItemByTitle('Test note')).toBeVisible();
|
||||
|
||||
// Focus the editor
|
||||
const markdownEditor = await editor.showMarkdownEditor();
|
||||
await markdownEditor.container.click();
|
||||
|
||||
// Type some text
|
||||
await mainWindow.keyboard.type('# Test note!');
|
||||
await mainWindow.keyboard.press('Enter');
|
||||
await mainWindow.keyboard.press('Enter');
|
||||
await mainWindow.keyboard.type('New note content!');
|
||||
|
||||
// Should render
|
||||
const viewer = await editor.showNoteViewer();
|
||||
await expect(viewer.content.locator('h1')).toHaveText('Test note!');
|
||||
});
|
||||
|
||||
test('mermaid and KaTeX should render', async ({ mainWindow }) => {
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
const editor = await mainScreen.createNewNote('🚧 Test 🚧');
|
||||
|
||||
const testCommitId = 'bf59b2';
|
||||
const markdownEditor = await editor.showMarkdownEditor();
|
||||
await markdownEditor.focusContent();
|
||||
const noteText = [
|
||||
'```mermaid',
|
||||
'gitGraph',
|
||||
' commit id: "973193"',
|
||||
` commit id: "${testCommitId}"`,
|
||||
' branch dev',
|
||||
' checkout dev',
|
||||
' commit id: "ceea77"',
|
||||
'```',
|
||||
'',
|
||||
'',
|
||||
'KaTeX:',
|
||||
'$$ \\int_0^1 \\cos(2x + 3) $$',
|
||||
'',
|
||||
'Sum: $\\sum_{x=0}^{100} \\tan x$',
|
||||
];
|
||||
let firstLine = true;
|
||||
for (const line of noteText) {
|
||||
if (line) {
|
||||
if (!firstLine) {
|
||||
// Remove any auto-indentation, but avoid pressing shift-tab at
|
||||
// the beginning of the editor.
|
||||
await markdownEditor.pressKey('Shift+Tab');
|
||||
}
|
||||
|
||||
await markdownEditor.typeText(line);
|
||||
}
|
||||
await mainWindow.keyboard.press('Enter');
|
||||
firstLine = false;
|
||||
}
|
||||
|
||||
// Should render mermaid
|
||||
const viewer = await editor.showNoteViewer();
|
||||
await expect(
|
||||
viewer.content.locator('pre.mermaid text', { hasText: testCommitId }),
|
||||
).toBeVisible();
|
||||
|
||||
// Should render KaTeX (block)
|
||||
// toBeAttached: To be added to the DOM.
|
||||
await expect(viewer.content.locator('.joplin-editable > .katex-display').first()).toBeAttached();
|
||||
await expect(
|
||||
viewer.content.locator(
|
||||
'.katex-display *', { hasText: 'cos' },
|
||||
).last(),
|
||||
).toBeVisible();
|
||||
|
||||
// Should render KaTeX (inline)
|
||||
await expect(viewer.content.locator('.joplin-editable > .katex').first()).toBeAttached();
|
||||
});
|
||||
|
||||
test('should correctly resize large images', async ({ electronApp, mainWindow }) => {
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
await mainScreen.createNewNote('Image resize test (part 1)');
|
||||
const editor = mainScreen.noteEditor;
|
||||
|
||||
await editor.showMarkdownEditor();
|
||||
|
||||
const filename = 'large-jpg-image-with-exif-rotation.jpg';
|
||||
await setFilePickerResponse(electronApp, [join(__dirname, 'resources', filename)]);
|
||||
|
||||
// Should be possible to cancel attaching for large images
|
||||
await setMessageBoxResponse(electronApp, /^Cancel/i);
|
||||
await editor.attachFileButton.click();
|
||||
await editor.expectToHaveText('\n');
|
||||
|
||||
// Clicking "No" should not resize
|
||||
await setMessageBoxResponse(electronApp, /^No/i);
|
||||
await editor.attachFileButton.click();
|
||||
|
||||
const viewer = await editor.showNoteViewer();
|
||||
const renderedImage = viewer.content
|
||||
.getByAltText(filename)
|
||||
// Work around occasional "resolved to 2 elements" errors in CI
|
||||
.last();
|
||||
|
||||
const fullSize = await getImageSourceSize(renderedImage);
|
||||
|
||||
// To make it easier to find the image (one image per note), we switch to a new, empty note.
|
||||
await mainScreen.createNewNote('Image resize test (part 2)');
|
||||
await editor.showMarkdownEditor();
|
||||
|
||||
// Clicking "Yes" should resize
|
||||
await setMessageBoxResponse(electronApp, /^Yes/i);
|
||||
await editor.attachFileButton.click();
|
||||
|
||||
const resizedSize = await getImageSourceSize(renderedImage);
|
||||
expect(resizedSize[0]).toBeLessThan(fullSize[0]);
|
||||
expect(resizedSize[1]).toBeLessThan(fullSize[1]);
|
||||
|
||||
// Should keep aspect ratio (regression test for #9597)
|
||||
expect(fullSize[0] / resizedSize[0]).toBeCloseTo(fullSize[1] / resizedSize[1]);
|
||||
});
|
||||
|
||||
for (const target of ['', '_blank']) {
|
||||
test(`clicking on an external link with target=${JSON.stringify(target)} should try to launch a browser`, async ({ electronApp, mainWindow }) => {
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
await mainScreen.waitFor();
|
||||
|
||||
// Mock openExternal
|
||||
const nextExternalUrlPromise = electronApp.evaluate(({ shell }) => {
|
||||
return new Promise<string>(resolve => {
|
||||
const openExternal = async (url: string) => {
|
||||
resolve(url);
|
||||
};
|
||||
shell.openExternal = openExternal;
|
||||
});
|
||||
});
|
||||
|
||||
// Create a test link
|
||||
const testLinkTitle = 'This is a test link!';
|
||||
const linkHref = 'https://joplinapp.org/';
|
||||
|
||||
await mainWindow.evaluate(({ testLinkTitle, linkHref, target }) => {
|
||||
const testLink = document.createElement('a');
|
||||
testLink.textContent = testLinkTitle;
|
||||
testLink.onclick = () => {
|
||||
// We need to navigate by setting location.href -- clicking on a link
|
||||
// directly within the main window (i.e. not in a PDF viewer) doesn't
|
||||
// navigate.
|
||||
location.href = linkHref;
|
||||
};
|
||||
testLink.href = '#';
|
||||
|
||||
// Display on top of everything
|
||||
testLink.style.zIndex = '99999';
|
||||
testLink.style.position = 'fixed';
|
||||
testLink.style.top = '0';
|
||||
testLink.style.left = '0';
|
||||
if (target) {
|
||||
testLink.target = target;
|
||||
}
|
||||
|
||||
document.body.appendChild(testLink);
|
||||
}, { testLinkTitle, linkHref, target });
|
||||
|
||||
const testLink = mainWindow.getByText(testLinkTitle);
|
||||
await expect(testLink).toBeVisible();
|
||||
await testLink.click({ noWaitAfter: true });
|
||||
|
||||
expect(await nextExternalUrlPromise).toBe(linkHref);
|
||||
});
|
||||
}
|
||||
|
||||
test('should start in safe mode if profile-dir/force-safe-mode-on-next-start exists', async ({ profileDirectory }) => {
|
||||
await writeFile(join(profileDirectory, 'force-safe-mode-on-next-start'), 'true', 'utf8');
|
||||
|
||||
// We need to write to the force-safe-mode file before opening the Electron app.
|
||||
// Open the app ourselves:
|
||||
const startupArgs = createStartupArgs(profileDirectory);
|
||||
const electronApp = await electron.launch({ args: startupArgs });
|
||||
const mainWindow = await getMainWindow(electronApp);
|
||||
|
||||
const safeModeDisableLink = mainWindow.getByText('Disable safe mode and restart');
|
||||
await safeModeDisableLink.waitFor();
|
||||
await expect(safeModeDisableLink).toBeInViewport();
|
||||
|
||||
await electronApp.close();
|
||||
});
|
||||
|
||||
test('should import an HTML directory', async ({ mainWindow, electronApp }) => {
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
await mainScreen.waitFor();
|
||||
|
||||
await mainScreen.importHtmlDirectory(electronApp, join(__dirname, 'resources', 'html-import'));
|
||||
const importedFolder = mainScreen.sidebar.container.getByText('html-import');
|
||||
await importedFolder.click();
|
||||
await mainScreen.noteList.focusContent(electronApp);
|
||||
|
||||
const importedNote1 = mainScreen.noteList.getNoteItemByTitle('test-html-file-with-image');
|
||||
const importedNote2 = mainScreen.noteList.getNoteItemByTitle('test-html-file-2');
|
||||
await expect.poll(async () => importedNote1.count(), { timeout: 60_000 }).toBeGreaterThan(0);
|
||||
await expect.poll(async () => importedNote2.count(), { timeout: 60_000 }).toBeGreaterThan(0);
|
||||
|
||||
await expect(importedNote1).toBeVisible();
|
||||
await expect(importedNote2).toBeVisible();
|
||||
});
|
||||
|
||||
test('should import a single HTML file', async ({ mainWindow, electronApp }) => {
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
await mainScreen.waitFor();
|
||||
|
||||
await mainScreen.importHtmlFile(electronApp, join(__dirname, 'resources', 'html-import', 'test-html-file-with-image.html'));
|
||||
|
||||
const importedNote = mainScreen.noteList.getNoteItemByTitle('test-html-file-with-image');
|
||||
await expect.poll(async () => importedNote.count(), { timeout: 60_000 }).toBeGreaterThan(0);
|
||||
await expect(importedNote).toBeVisible({ timeout: 60_000 });
|
||||
});
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户