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

这个提交包含在:
chen
2026-11-04 11:29:39 +08:00
当前提交 301194963b
共有 8663 个文件被更改,包括 1962691 次插入 和 0 次删除

查看文件

@@ -0,0 +1,23 @@
import { ElectronApplication, Locator, Page } from '@playwright/test';
import MainScreen from './MainScreen';
import activateMainMenuItem from '../util/activateMainMenuItem';
export default class ChangeAppLayoutScreen {
public readonly containerLocator: Locator;
public constructor(page: Page, private readonly mainScreen: MainScreen) {
this.containerLocator = page.locator('.change-app-layout-dialog[open]');
}
public async open(electronApp: ElectronApplication) {
await this.mainScreen.waitFor();
await activateMainMenuItem(electronApp, 'Change application layout');
return this.waitFor();
}
public async waitFor() {
await this.containerLocator.waitFor();
}
}

查看文件

@@ -0,0 +1,40 @@
import { ElectronApplication, Locator, Page } from '@playwright/test';
import setSettingValue from '../util/setSettingValue';
import MainScreen from './MainScreen';
import { expect } from '../util/test';
export default class ChatPanel {
public readonly container: Locator;
public readonly textInput: Locator;
public constructor(private page_: Page, private mainScreen_: MainScreen) {
this.container = page_.getByRole('region', { name: 'AI Chat' });
this.textInput = page_.getByRole('textbox', { name: 'Chat message' });
}
public async configure(electronApp: ElectronApplication) {
await setSettingValue(electronApp, this.page_, 'ai.enabled', true);
await setSettingValue(electronApp, this.page_, 'ai.allowRemote', true);
await setSettingValue(electronApp, this.page_, 'ai.chat.providerType', 'test-provider');
}
public async open(electronApp: ElectronApplication) {
await this.mainScreen_.goToAnything.runCommand(electronApp, 'toggleAiChat');
await this.container.waitFor();
}
public async close(electronApp: ElectronApplication) {
await this.container.waitFor();
await this.mainScreen_.goToAnything.runCommand(electronApp, 'toggleAiChat');
await expect(this.container).not.toBeVisible();
}
public async sendMessage(message: string) {
await this.textInput.fill(message);
await this.textInput.press('Enter');
}
public async waitForMessageCount(count: number) {
await expect(this.container.locator('.chat-message.turn')).toHaveCount(count);
}
}

查看文件

@@ -0,0 +1,92 @@
import { ElectronApplication, expect, Locator, Page } from '@playwright/test';
import MainScreen from './MainScreen';
import activateMainMenuItem from '../util/activateMainMenuItem';
import { msleep } from '@joplin/utils/time';
export default class GoToAnything {
public readonly containerLocator: Locator;
public readonly inputLocator: Locator;
public constructor(page: Page, private readonly mainScreen: MainScreen) {
this.containerLocator = page.locator('.go-to-anything-dialog[open]');
this.inputLocator = this.containerLocator.getByRole('textbox');
}
public async waitFor(timeout?: number) {
await this.containerLocator.waitFor({ timeout });
}
public async open(electronApp: ElectronApplication) {
await this.mainScreen.waitFor();
// The menu item toggles the dialog, so retrying unconditionally would close a dialog
// that a previous slow attempt had just opened.
await expect.poll(async () => {
if (await this.containerLocator.isVisible()) return true;
await activateMainMenuItem(electronApp, 'Goto Anything...');
try {
await this.waitFor(5_000);
} catch (error) {
return false;
}
return true;
}, {
timeout: 30_000,
message: 'should open the Goto Anything dialog',
}).toBe(true);
// Filling the controlled input before React attaches onChange silently drops the text.
await this.inputLocator.waitFor();
}
public async openLinkToNote(electronApp: ElectronApplication) {
await this.mainScreen.waitFor();
await activateMainMenuItem(electronApp, 'Link to note...');
return this.waitFor();
}
public resultLocator(resultText: string|RegExp) {
return this.containerLocator.getByRole('option', { name: resultText });
}
public async searchForWithRetry(query: string, resultLocator: Locator) {
// If note indexing hasn't finished, it's sometimes necessary to search multiple times.
// This expect.poll retries the search if it initially fails.
await expect.poll(async () => {
await this.inputLocator.clear();
// Pause to help ensure that the change in the search input is detected
await msleep(300);
await this.inputLocator.fill(query);
try {
await expect(resultLocator).toBeVisible({ timeout: 1000 });
} catch (error) {
// Return, rather than throw, the error -- expect.poll doesn't retry
// if the callback throws.
return error;
}
return true;
}, { timeout: 15_000 }).toBe(true);
}
public async expectToBeClosed() {
await expect(this.containerLocator).not.toBeAttached();
}
public async expectToBeOpen() {
await expect(this.containerLocator).toBeAttached();
}
public async runCommand(electronApp: ElectronApplication, command: string) {
if (!command.startsWith(':')) {
command = `:${command}`;
}
await this.open(electronApp);
await this.inputLocator.fill(command);
await this.containerLocator.locator('.match-highlight').first().waitFor();
await this.inputLocator.press('Enter');
await this.expectToBeClosed();
}
}

查看文件

@@ -0,0 +1,12 @@
import { Locator, Page } from '@playwright/test';
export default class JoplinCloudLoginScreen {
public readonly copyLinkToWebsiteLocator: Locator;
public constructor(private page_: Page) {
this.copyLinkToWebsiteLocator = this.page_.getByRole('button', { name: 'Copy link to website' });
}
public async waitFor() {
await this.copyLinkToWebsiteLocator.waitFor();
}
}

查看文件

@@ -0,0 +1,108 @@
import { Page, Locator, ElectronApplication } from '@playwright/test';
import NoteEditorScreen from './NoteEditor/NoteEditorScreen';
import activateMainMenuItem from '../util/activateMainMenuItem';
import Sidebar from './Sidebar';
import GoToAnything from './GoToAnything';
import setFilePickerResponse from '../util/setFilePickerResponse';
import NoteList from './NoteList';
import { expect } from '../util/test';
import ChangeAppLayoutScreen from './ChangeAppLayoutScreen';
import waitForNextWindowMatching from '../util/waitForNextWindowMatching';
import ChatPanel from './ChatPanel';
export default class MainScreen {
public readonly newNoteButton: Locator;
public readonly noteList: NoteList;
public readonly sidebar: Sidebar;
public readonly dialog: Locator;
public readonly noteEditor: NoteEditorScreen;
public readonly goToAnything: GoToAnything;
public readonly changeLayoutScreen: ChangeAppLayoutScreen;
public readonly chatPanel: ChatPanel;
public constructor(private page: Page) {
this.newNoteButton = page.locator('.new-note-button');
this.noteList = new NoteList(page);
this.sidebar = new Sidebar(page, this);
this.dialog = page.locator('.dialog-modal-layer');
this.noteEditor = new NoteEditorScreen(page);
this.goToAnything = new GoToAnything(page, this);
this.changeLayoutScreen = new ChangeAppLayoutScreen(page, this);
this.chatPanel = new ChatPanel(page, this);
}
public async setup() {
await this.waitFor();
const folder = await this.sidebar.createNewFolder('Test');
await folder.waitFor();
return this;
}
public async waitFor() {
await this.noteList.waitFor();
}
// Follows the steps a user would use to create a new note.
public async createNewNote(title: string) {
await this.waitFor();
// The new note button is only visible when a folder is selected -- wait for it explicitly.
await this.newNoteButton.waitFor();
// Create the new note. Retry this -- creating new notes can sometimes fail if done just after
// application startup.
await expect.poll(async () => {
await this.newNoteButton.click();
await expect(this.noteEditor.noteTitleInput).toHaveValue('', { timeout: 4_000 });
await expect(this.noteEditor.noteTitleInput).toHaveJSProperty('placeholder', 'Creating new note...', { timeout: 4_000 });
return true;
}, { timeout: 10_000 }).toBe(true);
// Fill the title
await this.noteEditor.noteTitleInput.click();
await this.noteEditor.noteTitleInput.fill(title);
// Wait for the title to reach the note list, otherwise a later sort can run
// while the note is still "Untitled" and order the list unexpectedly.
if (title !== '') {
await this.noteList.getNoteItemByTitle(title).waitFor();
}
return this.noteEditor;
}
public async openSettings(electronApp: ElectronApplication) {
// Check both labels so this works on MacOS
await activateMainMenuItem(electronApp, /^(Preferences\.\.\.|Options)$/);
}
public async openNewWindow(electronApp: ElectronApplication) {
const pagePromise = waitForNextWindowMatching(/^Joplin -/, electronApp);
await activateMainMenuItem(electronApp, 'Open in new window');
return pagePromise;
}
public async search(text: string) {
const searchBar = this.page.getByPlaceholder('Search...');
await searchBar.fill(text);
}
private async importFromModule_(electronApp: ElectronApplication, moduleName: string, path: string) {
await setFilePickerResponse(electronApp, [path]);
await activateMainMenuItem(electronApp, moduleName, 'Import');
}
public async importHtmlDirectory(electronApp: ElectronApplication, path: string) {
return this.importFromModule_(electronApp, 'HTML - HTML document (Directory)', path);
}
public async importHtmlFile(electronApp: ElectronApplication, path: string) {
return this.importFromModule_(electronApp, 'HTML - HTML document (File)', path);
}
public async pluginPanelLocator(pluginId: string) {
return this.page.locator(
`iframe[id^=${JSON.stringify(`plugin-view-${pluginId}`)}]`,
);
}
}

查看文件

@@ -0,0 +1,5 @@
interface BaseEditor {
typeText(text: string): Promise<void>;
waitFor(): Promise<void>;
}
export default BaseEditor;

查看文件

@@ -0,0 +1,22 @@
import { Locator, Page } from '@playwright/test';
export default class EditorCodeDialog {
private readonly dialog: Locator;
public readonly textArea: Locator;
public readonly okButton: Locator;
public constructor(page: Page) {
this.dialog = page.getByRole('dialog', { name: 'Edit' });
this.textArea = this.dialog.locator('textarea');
this.okButton = this.dialog.getByRole('button', { name: 'OK' });
}
public async waitFor() {
await this.dialog.waitFor();
await this.textArea.waitFor();
}
public async submit() {
await this.okButton.click();
}
}

查看文件

@@ -0,0 +1,33 @@
import { Locator } from '@playwright/test';
import BaseEditor from './BaseEditor';
export default class MarkdownEditor implements BaseEditor {
public readonly container: Locator;
public readonly content: Locator;
public constructor(parent: Locator) {
this.container = parent.locator('.CodeMirror');
this.content = this.container.locator('[contentEditable]');
}
public async waitFor() {
await this.container.waitFor();
}
public async pressKey(text: string) {
await this.content.press(text);
}
public async typeText(text: string) {
await this.focusContent();
await this.content.pressSequentially(text);
}
public async focusContent() {
// eslint-disable-next-line no-restricted-properties -- The focusHandler wrapper can't be used here, since Playwright's focus needs to be awaited
await this.content.focus();
}
public async innerText() {
return this.content.innerText();
}
}

查看文件

@@ -0,0 +1,203 @@
import { ElectronApplication, Locator, Page } from '@playwright/test';
import { expect } from '../../util/test';
import activateMainMenuItem from '../../util/activateMainMenuItem';
import setSettingValue from '../../util/setSettingValue';
import NoteViewer from './NoteViewer';
import MarkdownEditor from './MarkdownEditor';
import RichTextEditor from './RichTextEditor';
export default class NoteEditorScreen {
public readonly editorPluginFrame: Locator;
public readonly noteTitleInput: Locator;
public readonly attachFileButton: Locator;
public readonly toggleCodeBlockButton: Locator;
public readonly toggleEditorsButton: Locator;
public readonly toggleEditorLayoutButton: Locator;
private readonly disableTabNavigationButton: Locator;
public readonly toggleEditorPluginButton: Locator;
public readonly editorSearchInput: Locator;
public readonly viewerSearchInput: Locator;
private readonly containerLocator: Locator;
private readonly noteViewer_: NoteViewer;
private readonly markdownEditor_: MarkdownEditor;
private readonly richTextEditor_: RichTextEditor;
public constructor(private page_: Page) {
// .rli-editor is used in the main window, .note-editor-wrapper in secondary windows
this.containerLocator = page_.locator('.rli-editor, .note-editor-wrapper');
this.noteViewer_ = new NoteViewer(this.containerLocator);
this.markdownEditor_ = new MarkdownEditor(this.containerLocator);
this.richTextEditor_ = new RichTextEditor(this.containerLocator, page_);
this.editorPluginFrame = this.containerLocator.locator('iframe[id^="plugin-view-"]');
this.noteTitleInput = this.containerLocator.locator('.title-input');
this.attachFileButton = this.containerLocator.getByRole('button', { name: 'Attach file' });
this.toggleCodeBlockButton = this.containerLocator.getByRole('button', { name: 'Code Block' });
this.toggleEditorsButton = this.containerLocator.getByRole('button', { name: 'Toggle editors', exact: true });
this.toggleEditorLayoutButton = this.containerLocator.getByRole('button', { name: 'Toggle editor layout' });
// The editor and viewer have slightly different search UI
this.editorSearchInput = this.containerLocator.getByPlaceholder('Find');
this.viewerSearchInput = this.containerLocator.getByPlaceholder('Search...');
this.disableTabNavigationButton = this.containerLocator.getByRole('button', { name: 'Tab moves focus' });
this.toggleEditorPluginButton = this.containerLocator.getByRole('button', { name: 'Toggle editor plugin' });
}
public async waitFor() {
await this.contentLocator_();
await this.noteTitleInput.waitFor();
await this.toggleEditorsButton.waitFor();
}
public async undo(electronApp: ElectronApplication) {
await activateMainMenuItem(electronApp, 'Undo');
}
public toolbarButtonLocator(title: string) {
return this.containerLocator.getByRole('button', { name: title });
}
private async markdownEditorActive() {
await this.toggleEditorsButton.waitFor();
return this.toggleEditorsButton.evaluate(element => element.classList.contains('markdown-active'));
}
private async showMarkdownEditorOrViewer_() {
if (!await this.markdownEditorActive()) {
await this.toggleEditorsButton.click();
}
await Promise.race([
this.markdownEditor_.waitFor().catch(() => {}),
this.noteViewer_.waitFor().catch(() => {}),
]);
}
public async showNoteViewerAndMarkdownEditor() {
await this.showMarkdownEditorOrViewer_();
const noteViewerVisible = await this.noteViewer_.container.isVisible();
const noteEditorVisible = await this.markdownEditor_.container.isVisible();
if (noteViewerVisible && !noteEditorVisible) {
await this.toggleEditorLayout();
} else if (!noteViewerVisible && noteEditorVisible) {
await this.toggleEditorLayout();
await expect(this.noteViewer_.container).toBeVisible();
await expect(this.markdownEditor_.content).not.toBeVisible();
await this.toggleEditorLayout();
}
await expect(this.noteViewer_.container).toBeVisible();
await expect(this.markdownEditor_.content).toBeVisible();
return { viewer: this.noteViewer_, editor: this.markdownEditor_ };
}
public async showNoteViewer() {
await this.showMarkdownEditorOrViewer_();
if (!await this.noteViewer_.container.isVisible()) {
await this.toggleEditorLayout();
}
return this.noteViewer_;
}
public async showMarkdownEditor() {
await this.showMarkdownEditorOrViewer_();
if (!await this.markdownEditor_.container.isVisible()) {
await this.toggleEditorLayout();
}
return this.markdownEditor_;
}
public async showRichTextEditor() {
if (await this.markdownEditorActive()) {
await this.toggleEditorsButton.click();
}
await this.richTextEditor_.waitFor();
return this.richTextEditor_;
}
private async contentLocator_() {
const richTextBody = this.richTextEditor_.content.locator('body');
const markdownEditor = this.markdownEditor_.container;
const noteViewer = this.noteViewer_.container;
// Work around an issue where .or doesn't work with frameLocators.
// See https://github.com/microsoft/playwright/issues/27688#issuecomment-1771403495
await Promise.race([
richTextBody.waitFor({ state: 'visible' }).catch(()=>{}),
markdownEditor.waitFor({ state: 'visible' }).catch(()=>{}),
noteViewer.waitFor({ state: 'visible' }).catch(() => {}),
]);
if (await richTextBody.isVisible()) {
return richTextBody;
} else if (await markdownEditor.isVisible()) {
return markdownEditor;
} else {
return noteViewer;
}
}
public async disableInlineRendering(electronApp: ElectronApplication) {
await setSettingValue(electronApp, this.page_, 'editor.inlineRendering', false);
}
public async expectToHaveText(expected: string|RegExp) {
// expect(...).toHaveText can fail in the Rich Text Editor (perhaps due to frame locators).
// Using expect.poll refreshes the locator on each attempt, which seems to prevent flakiness.
const expectResult = expect.poll(
// Use .innerText: textContent doesn't handle line breaks correctly in the CodeMirror
// editor.
async () => (await this.contentLocator_()).innerText(),
);
// Allow `expected` to be either an exact match (a string) or a pattern
if (typeof expected === 'string') {
await expectResult.toBe(expected);
} else {
await expectResult.toMatch(expected);
}
}
public async enableTabNavigation(electronApp: ElectronApplication) {
await expect(this.disableTabNavigationButton).not.toBeVisible();
await activateMainMenuItem(electronApp, 'Tab moves focus');
await expect(this.disableTabNavigationButton).toBeVisible();
}
public async disableTabNavigation(electronApp: ElectronApplication) {
await expect(this.disableTabNavigationButton).toBeVisible();
await activateMainMenuItem(electronApp, 'Tab moves focus');
await expect(this.disableTabNavigationButton).not.toBeVisible();
}
public async goBack() {
const backButton = this.toolbarButtonLocator('Back');
await expect(backButton).not.toBeDisabled();
await backButton.click();
}
public async toggleEditorLayout() {
await this.toggleEditorLayoutButton.click();
}
public async hideViewer() {
const editorVisible = await this.markdownEditor_.container.isVisible();
await expect(this.noteViewer_.container).toBeVisible();
await this.toggleEditorLayout();
// An additional toggle was needed if initially in viewer-only mode
if (!editorVisible) {
await expect(this.markdownEditor_.container).toBeVisible();
await this.toggleEditorLayout();
}
await expect(this.noteViewer_.container).not.toBeVisible();
}
}

查看文件

@@ -0,0 +1,15 @@
import { FrameLocator, Locator } from '@playwright/test';
export default class NoteViewer {
public container: Locator;
public content: FrameLocator;
public constructor(parent: Locator) {
this.container = parent.locator('iframe[src$="note-viewer/index.html"]');
this.content = this.container.contentFrame();
}
public async waitFor() {
await this.container.waitFor();
await this.content.locator('body').waitFor();
}
}

查看文件

@@ -0,0 +1,44 @@
import { FrameLocator, Locator, Page } from '@playwright/test';
import BaseEditor from './BaseEditor';
import EditorCodeDialog from './EditorCodeDialog';
export default class RichTextEditor implements BaseEditor {
public readonly container: Locator;
public readonly content: FrameLocator;
public readonly codeEditor: EditorCodeDialog;
public readonly body: Locator;
public constructor(parent: Locator, page: Page) {
this.container = parent.locator('iframe[title="Rich Text Area"]');
this.content = this.container.contentFrame();
this.codeEditor = new EditorCodeDialog(page);
this.body = this.content.locator('body');
}
public async waitFor() {
await this.container.waitFor();
await this.content.locator('body').waitFor();
}
public async focusContent() {
// eslint-disable-next-line no-restricted-properties -- The focusHandler wrapper can't be used here, since Playwright's focus needs to be awaited
await this.body.focus();
}
public async typeText(text: string) {
await this.body.waitFor();
await this.focusContent();
await this.body.pressSequentially(text);
}
public async getSearchMatches() {
return this.body.evaluate(() => {
const highlights = CSS.highlights.get('jop-search-highlight') ?? new Set();
const result = [];
for (const highlight of highlights) {
result.push(highlight.toString());
}
return result;
});
}
}

查看文件

@@ -0,0 +1,43 @@
import activateMainMenuItem from '../util/activateMainMenuItem';
import { ElectronApplication, Locator, Page, expect } from '@playwright/test';
export default class NoteList {
public readonly container: Locator;
public readonly sortOrderButton: Locator;
public constructor(page: Page) {
this.container = page.locator('.rli-noteList');
this.sortOrderButton = this.container.getByRole('button', { name: 'Toggle sort order' });
}
public waitFor() {
return this.container.waitFor();
}
private async sortBy(electronApp: ElectronApplication, sortMethod: string) {
await activateMainMenuItem(electronApp, sortMethod, 'Sort notes by');
}
public async sortByTitle(electronApp: ElectronApplication) {
await this.sortBy(electronApp, 'Title');
await expect(this.sortOrderButton).toHaveAttribute('title', /Toggle sort order field:[\n ]*title ->/);
}
public async focusContent(electronApp: ElectronApplication) {
await activateMainMenuItem(electronApp, 'Note list', 'Focus');
await expect(this.container.locator(':focus')).toBeAttached();
// Wait for the list to have rendered a selected item before the caller
// tries to navigate with arrow keys. Without this, the aria-selected
// attribute may not be set yet on slow CI runners.
await expect(this.container.locator('[aria-selected="true"]')).toBeAttached();
}
// The resultant locator may fail to resolve if the item is not visible
public getNoteItemByTitle(title: string|RegExp) {
return this.container.getByRole('option', { name: title });
}
public async expectNoteToBeSelected(title: string|RegExp) {
await expect(this.getNoteItemByTitle(title)).toHaveAttribute('aria-selected', 'true');
}
}

查看文件

@@ -0,0 +1,36 @@
import { Page, Locator } from '@playwright/test';
import SyncTab from './SyncTab';
export default class SettingsScreen {
private readonly container: Locator;
public readonly okayButton: Locator;
public readonly appearanceTabButton: Locator;
public constructor(page: Page) {
this.container = page.locator('.config-screen');
this.okayButton = this.container.locator('.button-bar button', { hasText: 'OK' });
this.appearanceTabButton = this.getTabLocator('Appearance');
}
public getTabLocator(tabName: string) {
return this.container.getByRole('tab', { name: tabName });
}
public getLastTab() {
return this.container.getByRole('tablist').getByRole('tab').last();
}
public async waitFor() {
await this.okayButton.waitFor();
await this.appearanceTabButton.waitFor();
}
public async openSyncTab() {
await this.waitFor();
const syncTab = this.getTabLocator('Synchronisation');
await syncTab.click();
return new SyncTab(this.container);
}
}

查看文件

@@ -0,0 +1,13 @@
import { Locator } from '@playwright/test';
export default class SyncTab {
public readonly syncWizardButton: Locator;
public readonly syncTargetDropdown: Locator;
public readonly connectToJoplinCloudButton: Locator;
public constructor(container: Locator) {
this.syncWizardButton = container.getByRole('button', { name: 'Open Sync Wizard' });
this.syncTargetDropdown = container.getByRole('combobox', { name: 'Synchronisation target' });
this.connectToJoplinCloudButton = container.getByRole('button', { name: 'Connect to Joplin Cloud' });
}
}

查看文件

@@ -0,0 +1,56 @@
import activateMainMenuItem from '../util/activateMainMenuItem';
import type MainScreen from './MainScreen';
import { ElectronApplication, Locator, Page } from '@playwright/test';
import expect from '../util/extendedExpect';
export default class Sidebar {
public readonly container: Locator;
public readonly allNotes: Locator;
public constructor(page: Page, private mainScreen: MainScreen) {
this.container = page.locator('.rli-sideBar');
this.allNotes = this.container.getByText('All notes');
}
public async createNewFolder(title: string) {
const newFolderButton = this.container.getByRole('button', { name: 'New' });
await newFolderButton.click();
const titleInput = this.mainScreen.dialog.getByLabel('Title');
await titleInput.fill(title);
const submitButton = this.mainScreen.dialog.getByRole('button', { name: 'OK' });
await submitButton.click();
return this.container.getByRole('treeitem', { name: title });
}
private async sortBy(electronApp: ElectronApplication, option: string) {
await activateMainMenuItem(electronApp, option, 'Sort notebooks by');
}
public async sortByDate(electronApp: ElectronApplication) {
return this.sortBy(electronApp, 'Updated date');
}
public async sortByTitle(electronApp: ElectronApplication) {
return this.sortBy(electronApp, 'Title');
}
public async forceUpdateSorting(electronApp: ElectronApplication) {
// By default, notebooks will not be in the correct position in the list for about 1 second.
// Change the notebook list sort order to force an immediate refresh.
await this.sortByDate(electronApp);
await this.sortByTitle(electronApp);
}
// Checks the indentation level of each folder. Useful for determining whether folders are subfolders.
public async expectToHaveDepths(folderToDepth: [Locator, number][]) {
for (let i = 0; i < folderToDepth.length; i++) {
const [folder, depth] = folderToDepth[i];
await expect(
folder, { message: `Folder ${i} should have depth ${depth}.` },
).toHaveJSProperty('ariaLevel', String(depth));
}
}
}