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,300 @@
|
||||
import * as React from 'react';
|
||||
import { createTempDir, mockMobilePlatform, setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
|
||||
import { act, fireEvent, render, screen, userEvent, waitFor } from '../../../../utils/testing/testingLibrary';
|
||||
|
||||
import PluginService, { PluginSettings, defaultPluginSetting } from '@joplin/lib/services/plugins/PluginService';
|
||||
import { writeFile } from 'fs-extra';
|
||||
import { join } from 'path';
|
||||
import shim, { MobilePlatform } from '@joplin/lib/shim';
|
||||
import { resetRepoApi } from './utils/useRepoApi';
|
||||
import { Store } from 'redux';
|
||||
import { AppState } from '../../../../utils/types';
|
||||
import createMockReduxStore from '../../../../utils/testing/createMockReduxStore';
|
||||
import WrappedPluginStates from './testUtils/WrappedPluginStates';
|
||||
import mockRepositoryApiConstructor from './testUtils/mockRepositoryApiConstructor';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import mockPluginServiceSetup from '../../../../utils/testing/mockPluginServiceSetup';
|
||||
|
||||
// Plugin install/uninstall tests have been observed to fail or time out in CI:
|
||||
jest.retryTimes(2);
|
||||
|
||||
let reduxStore: Store<AppState> = null;
|
||||
|
||||
const loadMockPlugin = async (id: string, name: string, version: string, pluginSettings: PluginSettings) => {
|
||||
const service = PluginService.instance();
|
||||
const pluginSource = `
|
||||
/* joplin-manifest:
|
||||
${JSON.stringify({
|
||||
id,
|
||||
manifest_version: 1,
|
||||
app_min_version: '1.4',
|
||||
name,
|
||||
description: 'Test plugin',
|
||||
platforms: ['mobile', 'desktop'],
|
||||
version,
|
||||
homepage_url: 'https://joplinapp.org',
|
||||
})}
|
||||
*/
|
||||
|
||||
joplin.plugins.register({
|
||||
onStart: async function() { },
|
||||
});
|
||||
`;
|
||||
const pluginPath = join(await createTempDir(), 'plugin.js');
|
||||
await writeFile(pluginPath, pluginSource, 'utf-8');
|
||||
await act(async () => {
|
||||
await service.loadAndRunPlugins([pluginPath], pluginSettings);
|
||||
});
|
||||
};
|
||||
|
||||
const abcPluginId = 'org.joplinapp.plugins.AbcSheetMusic';
|
||||
const backlinksPluginId = 'joplin.plugin.ambrt.backlinksToNote';
|
||||
|
||||
describe('PluginStates.installed', () => {
|
||||
beforeEach(async () => {
|
||||
jest.useRealTimers();
|
||||
|
||||
await setupDatabaseAndSynchronizer(0);
|
||||
await switchClient(0);
|
||||
reduxStore = createMockReduxStore();
|
||||
mockPluginServiceSetup(reduxStore);
|
||||
resetRepoApi();
|
||||
|
||||
await mockMobilePlatform(MobilePlatform.Android);
|
||||
await mockRepositoryApiConstructor();
|
||||
|
||||
// Fake timers are necessary to prevent a warning.
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
afterEach(async () => {
|
||||
for (const pluginId of PluginService.instance().pluginIds) {
|
||||
await act(() => PluginService.instance().unloadPlugin(pluginId));
|
||||
}
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it.each([
|
||||
MobilePlatform.Android,
|
||||
MobilePlatform.Ios,
|
||||
])('should not allow updating a plugin that is not recommended on iOS, but should on Android (on %s)', async (platform) => {
|
||||
await mockMobilePlatform(platform);
|
||||
expect(shim.mobilePlatform()).toBe(platform);
|
||||
await mockRepositoryApiConstructor();
|
||||
|
||||
const defaultPluginSettings: PluginSettings = {
|
||||
[abcPluginId]: defaultPluginSetting(),
|
||||
[backlinksPluginId]: defaultPluginSetting(),
|
||||
};
|
||||
|
||||
// Load an outdated recommended plugin
|
||||
await loadMockPlugin(abcPluginId, 'ABC Sheet Music', '0.0.1', defaultPluginSettings);
|
||||
expect(PluginService.instance().plugins[abcPluginId]).toBeTruthy();
|
||||
|
||||
// Load a plugin not marked as recommended
|
||||
await loadMockPlugin(backlinksPluginId, 'Backlinks to note', '0.0.1', defaultPluginSettings);
|
||||
expect(PluginService.instance().plugins[backlinksPluginId]).toBeTruthy();
|
||||
|
||||
const wrapper = render(
|
||||
<WrappedPluginStates
|
||||
initialPluginSettings={defaultPluginSettings}
|
||||
store={reduxStore}
|
||||
/>,
|
||||
);
|
||||
expect(await screen.findByText(/^ABC Sheet Music/)).toBeVisible();
|
||||
expect(await screen.findByText(/^Backlinks to note/)).toBeVisible();
|
||||
|
||||
const updateMarkers = await screen.findAllByText('Update available');
|
||||
|
||||
// Backlinks to note should not be updatable on iOS (it's not _recommended).
|
||||
// ABC Sheet Music should always be updatable
|
||||
if (platform === 'android') {
|
||||
expect(updateMarkers).toHaveLength(2);
|
||||
} else {
|
||||
expect(updateMarkers).toHaveLength(1);
|
||||
}
|
||||
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('should show the current plugin version on updatable plugins', async () => {
|
||||
const defaultPluginSettings: PluginSettings = { [abcPluginId]: defaultPluginSetting() };
|
||||
|
||||
const outdatedVersion = '0.0.1';
|
||||
await loadMockPlugin(abcPluginId, 'ABC Sheet Music', outdatedVersion, defaultPluginSettings);
|
||||
expect(PluginService.instance().plugins[abcPluginId]).toBeTruthy();
|
||||
|
||||
const wrapper = render(
|
||||
<WrappedPluginStates
|
||||
initialPluginSettings={defaultPluginSettings}
|
||||
store={reduxStore}
|
||||
/>,
|
||||
);
|
||||
|
||||
const abcSheetMusicCard = await screen.findByText(/^ABC Sheet Music/);
|
||||
expect(abcSheetMusicCard).toBeVisible();
|
||||
expect(await screen.findByText('Update available')).toBeVisible();
|
||||
expect(await screen.findByText(`v${outdatedVersion}`)).toBeVisible();
|
||||
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('should update the list of installed plugins when a plugin is installed and uninstalled', async () => {
|
||||
const pluginSettings: PluginSettings = { };
|
||||
|
||||
const wrapper = render(
|
||||
<WrappedPluginStates
|
||||
initialPluginSettings={pluginSettings}
|
||||
store={reduxStore}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Initially, no plugins should be installed
|
||||
expect(screen.queryByText('Installed (0):')).toBeNull();
|
||||
|
||||
const testPluginId1 = 'org.joplinapp.plugins.AbcSheetMusic';
|
||||
const testPluginId2 = 'org.joplinapp.plugins.test.plugin.id';
|
||||
await act(() => loadMockPlugin(testPluginId1, 'ABC Sheet Music', '1.2.3', pluginSettings));
|
||||
await act(() => loadMockPlugin(testPluginId2, 'A test plugin', '1.0.0', pluginSettings));
|
||||
expect(PluginService.instance().plugins[testPluginId1]).toBeTruthy();
|
||||
|
||||
// Should update the list of installed plugins even though the plugin settings didn't change.
|
||||
expect(await screen.findByText(/^ABC Sheet Music/)).toBeVisible();
|
||||
expect(await screen.findByText(/^A test plugin/)).toBeVisible();
|
||||
|
||||
// Uninstalling one plugin should keep the other in the list
|
||||
await act(() => PluginService.instance().uninstallPlugin(testPluginId1));
|
||||
expect(await screen.findByText(/^A test plugin/)).toBeVisible();
|
||||
expect(screen.queryByText(/^ABC Sheet Music/)).toBeNull();
|
||||
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('should support disabling plugins from the info modal', async () => {
|
||||
const defaultPluginSettings: PluginSettings = { [abcPluginId]: defaultPluginSetting() };
|
||||
|
||||
await loadMockPlugin(abcPluginId, 'ABC Sheet Music', '1.2.3', defaultPluginSettings);
|
||||
expect(PluginService.instance().plugins[abcPluginId]).toBeTruthy();
|
||||
|
||||
const wrapper = render(
|
||||
<WrappedPluginStates
|
||||
initialPluginSettings={defaultPluginSettings}
|
||||
store={reduxStore}
|
||||
/>,
|
||||
);
|
||||
|
||||
const card = await screen.findByText('ABC Sheet Music');
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Open the plugin dialog
|
||||
await user.press(card);
|
||||
|
||||
const enabledSwitch = await screen.findByLabelText('Enabled');
|
||||
expect(enabledSwitch).toBeVisible();
|
||||
|
||||
// Use fireEvent instead of userEvent.press -- .press doesn't seem to work
|
||||
// for Switches. Similar issue: https://github.com/callstack/react-native-testing-library/issues/518.
|
||||
fireEvent(enabledSwitch, 'valueChange', false);
|
||||
|
||||
// The plugin should now be disabled
|
||||
await waitFor(() => {
|
||||
expect(Setting.value('plugins.states')).toMatchObject({
|
||||
[abcPluginId]: { enabled: false },
|
||||
});
|
||||
});
|
||||
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('should support updating plugins from the info modal', async () => {
|
||||
await mockRepositoryApiConstructor();
|
||||
|
||||
const defaultPluginSettings: PluginSettings = {
|
||||
[abcPluginId]: defaultPluginSetting(),
|
||||
};
|
||||
|
||||
// Load an outdated recommended plugin
|
||||
await loadMockPlugin(abcPluginId, 'ABC Sheet Music', '0.0.1', defaultPluginSettings);
|
||||
expect(PluginService.instance().plugins[abcPluginId]).toBeTruthy();
|
||||
|
||||
const wrapper = render(
|
||||
<WrappedPluginStates
|
||||
initialPluginSettings={defaultPluginSettings}
|
||||
store={reduxStore}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Open the plugin dialog
|
||||
const card = await screen.findByText('ABC Sheet Music');
|
||||
const user = userEvent.setup();
|
||||
await user.press(card);
|
||||
|
||||
const updateButton = await screen.findByRole('button', { name: 'Update' });
|
||||
expect(updateButton).toBeVisible();
|
||||
await user.press(updateButton);
|
||||
|
||||
// After updating, the update button should read "updated". Use a large
|
||||
// timeout because updating plugins can be slow, particularly in CI.
|
||||
const updatedButton = await screen.findByRole('button', { name: 'Updated', disabled: true }, { timeout: 16000 });
|
||||
expect(updatedButton).toBeVisible();
|
||||
|
||||
// Should be marked as updated.
|
||||
await waitFor(() => {
|
||||
expect(Setting.value('plugins.states')).toMatchObject({
|
||||
[abcPluginId]: { enabled: true, hasBeenUpdated: true },
|
||||
});
|
||||
});
|
||||
|
||||
// Simulate the behavior of the plugin loader -- unloading and reloading plugins is generally
|
||||
// handled elsewhere. This does, however, help verify that the verison number changes correctly
|
||||
// in the UI.
|
||||
await act(async () => {
|
||||
await PluginService.instance().unloadPlugin(abcPluginId);
|
||||
await loadMockPlugin(abcPluginId, 'ABC Sheet Music', '0.0.2', defaultPluginSettings);
|
||||
});
|
||||
|
||||
// Version should change in two places -- the plugin list and the modal.
|
||||
await waitFor(() => {
|
||||
const versionText = screen.getAllByText('v0.0.2');
|
||||
expect(versionText).toHaveLength(2);
|
||||
});
|
||||
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('should be possible to disable plugins, even if missing from plugins.states', async () => {
|
||||
await mockRepositoryApiConstructor();
|
||||
|
||||
const defaultPluginSettings: PluginSettings = {};
|
||||
await loadMockPlugin(abcPluginId, 'ABC Sheet Music', '3.4.5', defaultPluginSettings);
|
||||
expect(PluginService.instance().plugins[abcPluginId]).toBeTruthy();
|
||||
|
||||
const wrapper = render(
|
||||
<WrappedPluginStates
|
||||
initialPluginSettings={defaultPluginSettings}
|
||||
store={reduxStore}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Should be shown as installed.
|
||||
const card = await screen.findByText('ABC Sheet Music');
|
||||
expect(card).toBeVisible();
|
||||
|
||||
const user = userEvent.setup();
|
||||
await user.press(card);
|
||||
|
||||
// Should be considered installed -- should be possible to disable:
|
||||
const enabledSwitch = await screen.findByLabelText('Enabled');
|
||||
expect(enabledSwitch).toBeVisible();
|
||||
fireEvent(enabledSwitch, 'valueChange', false);
|
||||
|
||||
// Disabling should add the plugin to plugins.states, if not present before.
|
||||
await waitFor(() => {
|
||||
expect(Setting.value('plugins.states')).toMatchObject({
|
||||
[abcPluginId]: { enabled: false },
|
||||
});
|
||||
});
|
||||
|
||||
wrapper.unmount();
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户