initial commit
This commit is contained in:
parent
7629e56e28
commit
61e71a562c
10
.editorconfig
Normal file
10
.editorconfig
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.{js,json,yml,ts,tsx,jsx}]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 2
|
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/.yarn/** linguist-vendored
|
||||||
|
/.yarn/releases/* binary
|
||||||
|
/.yarn/plugins/**/* binary
|
||||||
|
/.pnp.* binary linguist-generated
|
27
.github/workflows/playwright.yml
vendored
Normal file
27
.github/workflows/playwright.yml
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
name: Playwright Tests
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main, master ]
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
timeout-minutes: 60
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: lts/*
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install -g yarn && yarn
|
||||||
|
- name: Install Playwright Browsers
|
||||||
|
run: yarn playwright install --with-deps
|
||||||
|
- name: Run Playwright tests
|
||||||
|
run: yarn playwright test
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: playwright-report/
|
||||||
|
retention-days: 30
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
lib
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"nuxt.isNuxtApp": false
|
||||||
|
}
|
BIN
.yarn/install-state.gz
vendored
Normal file
BIN
.yarn/install-state.gz
vendored
Normal file
Binary file not shown.
2
.yarnrc.yml
Normal file
2
.yarnrc.yml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
nmMode: classic
|
||||||
|
nodeLinker: node-modules
|
2
README.md
Normal file
2
README.md
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
# Typescript module
|
39
package.json
Normal file
39
package.json
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "obsidian-testing-framework",
|
||||||
|
"packageManager": "yarn@4.5.1",
|
||||||
|
"dependencies": {
|
||||||
|
"typescript": "^5.6.3"
|
||||||
|
},
|
||||||
|
"version": "",
|
||||||
|
"files": [
|
||||||
|
"./bin/*",
|
||||||
|
"./lib/*"
|
||||||
|
],
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"default": "./lib/index.js",
|
||||||
|
"types": "./lib/index.d.ts"
|
||||||
|
},
|
||||||
|
"./utils": {
|
||||||
|
"default": "./lib/utils.js",
|
||||||
|
"types": "./lib/utils.d.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"main": "./lib/index.js",
|
||||||
|
"typings": "./lib/index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"lint": "tslint -c tslint.json src/**/*.ts",
|
||||||
|
"prepublish": "npm run build"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.48.1",
|
||||||
|
"@types/node": "^22.7.8",
|
||||||
|
"obsidian": "latest",
|
||||||
|
"playwright": "^1.48.1",
|
||||||
|
"vitest": "^2.1.3"
|
||||||
|
},
|
||||||
|
"workspaces": [
|
||||||
|
"./*"
|
||||||
|
]
|
||||||
|
}
|
10
src/fixtures.ts
Normal file
10
src/fixtures.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { App } from "obsidian";
|
||||||
|
import { ElectronApplication, Page } from "playwright";
|
||||||
|
import { ObsidianTestingConfig } from "src";
|
||||||
|
|
||||||
|
export interface ObsidianTestFixtures {
|
||||||
|
electronApp: ElectronApplication;
|
||||||
|
page: Page;
|
||||||
|
app: App;
|
||||||
|
obsidian: ObsidianTestingConfig;
|
||||||
|
}
|
52
src/index.ts
Normal file
52
src/index.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { App } from "obsidian";
|
||||||
|
import { test as base } from "@playwright/test";
|
||||||
|
import { _electron as electron, ElectronApplication, Page } from "playwright";
|
||||||
|
import { Fixtures } from "@playwright/test";
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
import path from "path";
|
||||||
|
import { ObsidianTestFixtures } from "./fixtures";
|
||||||
|
|
||||||
|
export interface ObsidianTestingConfig {
|
||||||
|
vault?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getExe(): string {
|
||||||
|
if (process.platform == "win32") {
|
||||||
|
return path.join(process.env.LOCALAPPDATA!, "Obsidian", "Obsidian.exe");
|
||||||
|
}
|
||||||
|
if (process.platform == "darwin") {
|
||||||
|
throw new Error("use a non-toy operating system, dumbass");
|
||||||
|
}
|
||||||
|
return execSync("command -v obsidian").toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const obsidianTestFixtures: Fixtures<
|
||||||
|
ObsidianTestFixtures
|
||||||
|
> = {
|
||||||
|
electronApp: [
|
||||||
|
async ({ obsidian: {vault} }, run) => {
|
||||||
|
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = "true";
|
||||||
|
const electronApp = await electron.launch({
|
||||||
|
executablePath: getExe(),
|
||||||
|
args: [!!vault && `obsidian://open?vault=${encodeURI(vault)}`].filter(a => !!a) as string[],
|
||||||
|
});
|
||||||
|
await run(electronApp);
|
||||||
|
await electronApp.close();
|
||||||
|
},
|
||||||
|
{ auto: true },
|
||||||
|
],
|
||||||
|
page: [
|
||||||
|
async ({ electronApp }, run) => {
|
||||||
|
const page = await electronApp.firstWindow();
|
||||||
|
await run(page);
|
||||||
|
},
|
||||||
|
{ auto: true },
|
||||||
|
],
|
||||||
|
app: [async ({ page }, run) => {
|
||||||
|
const app = await page.evaluate<App>("window.app");
|
||||||
|
await run(app);
|
||||||
|
}, {auto: true}],
|
||||||
|
obsidian: [{}, {option: true}]
|
||||||
|
};
|
||||||
|
// @ts-ignore some error about a string type now having `undefined` as part of it's union
|
||||||
|
export const test = base.extend<ObsidianTestFixtures>(obsidianTestFixtures);
|
114
src/tester.ts
Normal file
114
src/tester.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import {
|
||||||
|
_electron,
|
||||||
|
_electron as electron,
|
||||||
|
ElectronApplication,
|
||||||
|
Page,
|
||||||
|
} from "playwright";
|
||||||
|
import {expect} from "vitest"
|
||||||
|
import { App, TFile, Vault } from "obsidian";
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
import path from "path";
|
||||||
|
import { EOL } from "os";
|
||||||
|
type RawOptions = NonNullable<Parameters<typeof _electron.launch>[0]>;
|
||||||
|
export type TestOptions = Omit<RawOptions, "executablePath"> & {
|
||||||
|
vault: string;
|
||||||
|
};
|
||||||
|
export class ObsidianTester {
|
||||||
|
#loadPromise: Promise<void>;
|
||||||
|
|
||||||
|
public electronApp: ElectronApplication | null = null;
|
||||||
|
public page: Page;
|
||||||
|
public app: App;
|
||||||
|
|
||||||
|
constructor(options: Partial<TestOptions & { vault: string }>) {
|
||||||
|
const args = [...(options.args || [])];
|
||||||
|
if (options.vault)
|
||||||
|
args.push(`obsidian://open?vault=${encodeURI(options.vault)}`);
|
||||||
|
this.#loadPromise = new Promise((res, rej) => {
|
||||||
|
electron
|
||||||
|
.launch({
|
||||||
|
...options,
|
||||||
|
args,
|
||||||
|
executablePath: ObsidianTester.getExe(),
|
||||||
|
})
|
||||||
|
.then((v) => {
|
||||||
|
this.postInit(v).then(() => {
|
||||||
|
res();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((e) => rej(e));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public get vault(): Vault {
|
||||||
|
return this.app.vault
|
||||||
|
}
|
||||||
|
|
||||||
|
public async doWithApp(
|
||||||
|
callback: (app: App) => Promise<unknown>
|
||||||
|
): Promise<unknown> {
|
||||||
|
await this.#loadPromise;
|
||||||
|
return await callback(this.app);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async doWithVault(
|
||||||
|
callback: (vault: Vault) => Promise<unknown>
|
||||||
|
): Promise<unknown> {
|
||||||
|
await this.#loadPromise;
|
||||||
|
return await callback(this.app.vault);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async assertFileEquals(path: string, expectedContent: string, cached: boolean = true) {
|
||||||
|
await this.#loadPromise;
|
||||||
|
const fileContent = await this.readFile(path, cached);
|
||||||
|
|
||||||
|
expect(fileContent).toEqual(this.normalizeEOL(expectedContent));
|
||||||
|
}
|
||||||
|
public async assertLineEquals(path: string, lineNumber: number, expectedContent: string, cached: boolean = true) {
|
||||||
|
await this.#loadPromise;
|
||||||
|
const fileContent = await this.readFile(path, cached);
|
||||||
|
|
||||||
|
expect(fileContent.split("\n")[lineNumber]).toEqual(this.normalizeEOL(expectedContent));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async assertLinesEqual(filePath: string, start: number, end: number, expected: string, cached: boolean = true) {
|
||||||
|
await this.#loadPromise;
|
||||||
|
const fileContent = await this.readFile(filePath, cached);
|
||||||
|
const lines = fileContent.split("\n").slice(start, end);
|
||||||
|
const expectedLines = this.normalizeEOL(expected).split("\n");
|
||||||
|
expect(lines.every((l, i) => l == expectedLines[i])).toEqual(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getFile(file: string): TFile {
|
||||||
|
let f = this.app.vault.getFileByPath(file);
|
||||||
|
if(!f) {
|
||||||
|
throw new Error("File does not exist in vault.");
|
||||||
|
}
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizeEOL(str: string): string {
|
||||||
|
return str.split(/\r\n|\r|\n/).join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
async readFile(path: string, cached: boolean = true): Promise<string> {
|
||||||
|
await this.#loadPromise;
|
||||||
|
const file = this.getFile(path);
|
||||||
|
return this.normalizeEOL(await (cached ? this.app.vault.cachedRead(file) : this.app.vault.read(file)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async postInit(electronApp: ElectronApplication) {
|
||||||
|
this.electronApp = electronApp;
|
||||||
|
this.page = await this.electronApp.firstWindow();
|
||||||
|
this.app = await this.page.evaluate<App>("window.app");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getExe(): string {
|
||||||
|
if (process.platform == "win32") {
|
||||||
|
return path.join(process.env.LOCALAPPDATA!, "Obsidian", "Obsidian.exe");
|
||||||
|
}
|
||||||
|
if (process.platform == "darwin") {
|
||||||
|
throw new Error("use a non-toy operating system, dumbass");
|
||||||
|
}
|
||||||
|
return execSync("command -v obsidian").toString();
|
||||||
|
}
|
||||||
|
}
|
65
src/util.ts
Normal file
65
src/util.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { App, TFile } from "obsidian";
|
||||||
|
import { PlaywrightTestArgs } from "playwright/test";
|
||||||
|
import { expect } from "vitest";
|
||||||
|
import { ObsidianTestFixtures } from "./fixtures";
|
||||||
|
import { test } from "src";
|
||||||
|
|
||||||
|
// type TestArgs = Parameters<Parameters<typeof test>[2]>[0];
|
||||||
|
|
||||||
|
export async function assertFileEquals(
|
||||||
|
app: App,
|
||||||
|
path: string,
|
||||||
|
expectedContent: string,
|
||||||
|
cached: boolean = true
|
||||||
|
) {
|
||||||
|
const fileContent = await readFile(app, path, cached);
|
||||||
|
|
||||||
|
expect(fileContent).toEqual(normalizeEOL(expectedContent));
|
||||||
|
}
|
||||||
|
export async function assertLineEquals(
|
||||||
|
app: App,
|
||||||
|
path: string,
|
||||||
|
lineNumber: number,
|
||||||
|
expectedContent: string,
|
||||||
|
cached: boolean = true
|
||||||
|
) {
|
||||||
|
const fileContent = await readFile(app, path, cached);
|
||||||
|
|
||||||
|
expect(fileContent.split("\n")[lineNumber]).toEqual(
|
||||||
|
normalizeEOL(expectedContent)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function assertLinesEqual(
|
||||||
|
app: App,
|
||||||
|
filePath: string,
|
||||||
|
start: number,
|
||||||
|
end: number,
|
||||||
|
expected: string,
|
||||||
|
cached: boolean = true
|
||||||
|
) {
|
||||||
|
const fileContent = await readFile(app, filePath, cached);
|
||||||
|
const lines = fileContent.split("\n").slice(start, end);
|
||||||
|
const expectedLines = normalizeEOL(expected).split("\n");
|
||||||
|
expect(lines.every((l, i) => l == expectedLines[i])).toEqual(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFile(app: App, file: string): TFile {
|
||||||
|
let f = app.vault.getFileByPath(file);
|
||||||
|
if (!f) {
|
||||||
|
throw new Error("File does not exist in vault.");
|
||||||
|
}
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeEOL(str: string): string {
|
||||||
|
return str.split(/\r\n|\r|\n/).join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readFile(app: App, path: string, cached: boolean = true): Promise<string> {
|
||||||
|
const file = getFile(app, path);
|
||||||
|
return normalizeEOL(
|
||||||
|
await (cached ? app.vault.cachedRead(file) : app.vault.read(file))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
10
test-project/.editorconfig
Normal file
10
test-project/.editorconfig
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
tab_width = 4
|
3
test-project/.eslintignore
Normal file
3
test-project/.eslintignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
|
||||||
|
main.js
|
23
test-project/.eslintrc
Normal file
23
test-project/.eslintrc
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"env": { "node": true },
|
||||||
|
"plugins": [
|
||||||
|
"@typescript-eslint"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/eslint-recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
|
||||||
|
"@typescript-eslint/ban-ts-comment": "off",
|
||||||
|
"no-prototype-builtins": "off",
|
||||||
|
"@typescript-eslint/no-empty-function": "off"
|
||||||
|
}
|
||||||
|
}
|
22
test-project/.gitignore
vendored
Normal file
22
test-project/.gitignore
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# vscode
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
# Intellij
|
||||||
|
*.iml
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# npm
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Don't include the compiled main.js file in the repo.
|
||||||
|
# They should be uploaded to GitHub releases instead.
|
||||||
|
main.js
|
||||||
|
|
||||||
|
# Exclude sourcemaps
|
||||||
|
*.map
|
||||||
|
|
||||||
|
# obsidian
|
||||||
|
data.json
|
||||||
|
|
||||||
|
# Exclude macOS Finder (System Explorer) View States
|
||||||
|
.DS_Store
|
1
test-project/.npmrc
Normal file
1
test-project/.npmrc
Normal file
@ -0,0 +1 @@
|
|||||||
|
tag-version-prefix=""
|
96
test-project/README.md
Normal file
96
test-project/README.md
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
# Obsidian Sample Plugin
|
||||||
|
|
||||||
|
This is a sample plugin for Obsidian (https://obsidian.md).
|
||||||
|
|
||||||
|
This project uses TypeScript to provide type checking and documentation.
|
||||||
|
The repo depends on the latest plugin API (obsidian.d.ts) in TypeScript Definition format, which contains TSDoc comments describing what it does.
|
||||||
|
|
||||||
|
**Note:** The Obsidian API is still in early alpha and is subject to change at any time!
|
||||||
|
|
||||||
|
This sample plugin demonstrates some of the basic functionality the plugin API can do.
|
||||||
|
- Adds a ribbon icon, which shows a Notice when clicked.
|
||||||
|
- Adds a command "Open Sample Modal" which opens a Modal.
|
||||||
|
- Adds a plugin setting tab to the settings page.
|
||||||
|
- Registers a global click event and output 'click' to the console.
|
||||||
|
- Registers a global interval which logs 'setInterval' to the console.
|
||||||
|
|
||||||
|
## First time developing plugins?
|
||||||
|
|
||||||
|
Quick starting guide for new plugin devs:
|
||||||
|
|
||||||
|
- Check if [someone already developed a plugin for what you want](https://obsidian.md/plugins)! There might be an existing plugin similar enough that you can partner up with.
|
||||||
|
- Make a copy of this repo as a template with the "Use this template" button (login to GitHub if you don't see it).
|
||||||
|
- Clone your repo to a local development folder. For convenience, you can place this folder in your `.obsidian/plugins/your-plugin-name` folder.
|
||||||
|
- Install NodeJS, then run `npm i` in the command line under your repo folder.
|
||||||
|
- Run `npm run dev` to compile your plugin from `main.ts` to `main.js`.
|
||||||
|
- Make changes to `main.ts` (or create new `.ts` files). Those changes should be automatically compiled into `main.js`.
|
||||||
|
- Reload Obsidian to load the new version of your plugin.
|
||||||
|
- Enable plugin in settings window.
|
||||||
|
- For updates to the Obsidian API run `npm update` in the command line under your repo folder.
|
||||||
|
|
||||||
|
## Releasing new releases
|
||||||
|
|
||||||
|
- Update your `manifest.json` with your new version number, such as `1.0.1`, and the minimum Obsidian version required for your latest release.
|
||||||
|
- Update your `versions.json` file with `"new-plugin-version": "minimum-obsidian-version"` so older versions of Obsidian can download an older version of your plugin that's compatible.
|
||||||
|
- Create new GitHub release using your new version number as the "Tag version". Use the exact version number, don't include a prefix `v`. See here for an example: https://github.com/obsidianmd/obsidian-sample-plugin/releases
|
||||||
|
- Upload the files `manifest.json`, `main.js`, `styles.css` as binary attachments. Note: The manifest.json file must be in two places, first the root path of your repository and also in the release.
|
||||||
|
- Publish the release.
|
||||||
|
|
||||||
|
> You can simplify the version bump process by running `npm version patch`, `npm version minor` or `npm version major` after updating `minAppVersion` manually in `manifest.json`.
|
||||||
|
> The command will bump version in `manifest.json` and `package.json`, and add the entry for the new version to `versions.json`
|
||||||
|
|
||||||
|
## Adding your plugin to the community plugin list
|
||||||
|
|
||||||
|
- Check the [plugin guidelines](https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines).
|
||||||
|
- Publish an initial version.
|
||||||
|
- Make sure you have a `README.md` file in the root of your repo.
|
||||||
|
- Make a pull request at https://github.com/obsidianmd/obsidian-releases to add your plugin.
|
||||||
|
|
||||||
|
## How to use
|
||||||
|
|
||||||
|
- Clone this repo.
|
||||||
|
- Make sure your NodeJS is at least v16 (`node --version`).
|
||||||
|
- `npm i` or `yarn` to install dependencies.
|
||||||
|
- `npm run dev` to start compilation in watch mode.
|
||||||
|
|
||||||
|
## Manually installing the plugin
|
||||||
|
|
||||||
|
- Copy over `main.js`, `styles.css`, `manifest.json` to your vault `VaultFolder/.obsidian/plugins/your-plugin-id/`.
|
||||||
|
|
||||||
|
## Improve code quality with eslint (optional)
|
||||||
|
- [ESLint](https://eslint.org/) is a tool that analyzes your code to quickly find problems. You can run ESLint against your plugin to find common bugs and ways to improve your code.
|
||||||
|
- To use eslint with this project, make sure to install eslint from terminal:
|
||||||
|
- `npm install -g eslint`
|
||||||
|
- To use eslint to analyze this project use this command:
|
||||||
|
- `eslint main.ts`
|
||||||
|
- eslint will then create a report with suggestions for code improvement by file and line number.
|
||||||
|
- If your source code is in a folder, such as `src`, you can use eslint with this command to analyze all files in that folder:
|
||||||
|
- `eslint .\src\`
|
||||||
|
|
||||||
|
## Funding URL
|
||||||
|
|
||||||
|
You can include funding URLs where people who use your plugin can financially support it.
|
||||||
|
|
||||||
|
The simple way is to set the `fundingUrl` field to your link in your `manifest.json` file:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"fundingUrl": "https://buymeacoffee.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you have multiple URLs, you can also do:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"fundingUrl": {
|
||||||
|
"Buy Me a Coffee": "https://buymeacoffee.com",
|
||||||
|
"GitHub Sponsor": "https://github.com/sponsors",
|
||||||
|
"Patreon": "https://www.patreon.com/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Documentation
|
||||||
|
|
||||||
|
See https://github.com/obsidianmd/obsidian-api
|
49
test-project/esbuild.config.mjs
Normal file
49
test-project/esbuild.config.mjs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import esbuild from "esbuild";
|
||||||
|
import process from "process";
|
||||||
|
import builtins from "builtin-modules";
|
||||||
|
|
||||||
|
const banner =
|
||||||
|
`/*
|
||||||
|
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||||
|
if you want to view the source, please visit the github repository of this plugin
|
||||||
|
*/
|
||||||
|
`;
|
||||||
|
|
||||||
|
const prod = (process.argv[2] === "production");
|
||||||
|
|
||||||
|
const context = await esbuild.context({
|
||||||
|
banner: {
|
||||||
|
js: banner,
|
||||||
|
},
|
||||||
|
entryPoints: ["main.ts"],
|
||||||
|
bundle: true,
|
||||||
|
external: [
|
||||||
|
"obsidian",
|
||||||
|
"electron",
|
||||||
|
"@codemirror/autocomplete",
|
||||||
|
"@codemirror/collab",
|
||||||
|
"@codemirror/commands",
|
||||||
|
"@codemirror/language",
|
||||||
|
"@codemirror/lint",
|
||||||
|
"@codemirror/search",
|
||||||
|
"@codemirror/state",
|
||||||
|
"@codemirror/view",
|
||||||
|
"@lezer/common",
|
||||||
|
"@lezer/highlight",
|
||||||
|
"@lezer/lr",
|
||||||
|
...builtins],
|
||||||
|
format: "cjs",
|
||||||
|
target: "es2018",
|
||||||
|
logLevel: "info",
|
||||||
|
sourcemap: prod ? false : "inline",
|
||||||
|
treeShaking: true,
|
||||||
|
outfile: "main.js",
|
||||||
|
minify: prod,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (prod) {
|
||||||
|
await context.rebuild();
|
||||||
|
process.exit(0);
|
||||||
|
} else {
|
||||||
|
await context.watch();
|
||||||
|
}
|
134
test-project/main.ts
Normal file
134
test-project/main.ts
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
|
||||||
|
|
||||||
|
// Remember to rename these classes and interfaces!
|
||||||
|
|
||||||
|
interface MyPluginSettings {
|
||||||
|
mySetting: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_SETTINGS: MyPluginSettings = {
|
||||||
|
mySetting: 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class MyPlugin extends Plugin {
|
||||||
|
settings: MyPluginSettings;
|
||||||
|
|
||||||
|
async onload() {
|
||||||
|
await this.loadSettings();
|
||||||
|
|
||||||
|
// This creates an icon in the left ribbon.
|
||||||
|
const ribbonIconEl = this.addRibbonIcon('dice', 'Sample Plugin', (evt: MouseEvent) => {
|
||||||
|
// Called when the user clicks the icon.
|
||||||
|
new Notice('This is a notice!', 0);
|
||||||
|
});
|
||||||
|
// Perform additional things with the ribbon
|
||||||
|
ribbonIconEl.addClass('my-plugin-ribbon-class');
|
||||||
|
|
||||||
|
// This adds a status bar item to the bottom of the app. Does not work on mobile apps.
|
||||||
|
const statusBarItemEl = this.addStatusBarItem();
|
||||||
|
statusBarItemEl.setText('Status Bar Text');
|
||||||
|
|
||||||
|
// This adds a simple command that can be triggered anywhere
|
||||||
|
this.addCommand({
|
||||||
|
id: 'open-sample-modal-simple',
|
||||||
|
name: 'Open sample modal (simple)',
|
||||||
|
callback: () => {
|
||||||
|
new SampleModal(this.app).open();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// This adds an editor command that can perform some operation on the current editor instance
|
||||||
|
this.addCommand({
|
||||||
|
id: 'sample-editor-command',
|
||||||
|
name: 'Sample editor command',
|
||||||
|
editorCallback: (editor: Editor, view: MarkdownView) => {
|
||||||
|
console.log(editor.getSelection());
|
||||||
|
editor.replaceSelection('Sample Editor Command');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// This adds a complex command that can check whether the current state of the app allows execution of the command
|
||||||
|
this.addCommand({
|
||||||
|
id: 'open-sample-modal-complex',
|
||||||
|
name: 'Open sample modal (complex)',
|
||||||
|
checkCallback: (checking: boolean) => {
|
||||||
|
// Conditions to check
|
||||||
|
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||||
|
if (markdownView) {
|
||||||
|
// If checking is true, we're simply "checking" if the command can be run.
|
||||||
|
// If checking is false, then we want to actually perform the operation.
|
||||||
|
if (!checking) {
|
||||||
|
new SampleModal(this.app).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
// This command will only show up in Command Palette when the check function returns true
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// This adds a settings tab so the user can configure various aspects of the plugin
|
||||||
|
this.addSettingTab(new SampleSettingTab(this.app, this));
|
||||||
|
|
||||||
|
// If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this plugin)
|
||||||
|
// Using this function will automatically remove the event listener when this plugin is disabled.
|
||||||
|
this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
|
||||||
|
console.log('click', evt);
|
||||||
|
});
|
||||||
|
|
||||||
|
// When registering intervals, this function will automatically clear the interval when the plugin is disabled.
|
||||||
|
this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
onunload() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadSettings() {
|
||||||
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveSettings() {
|
||||||
|
await this.saveData(this.settings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SampleModal extends Modal {
|
||||||
|
constructor(app: App) {
|
||||||
|
super(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpen() {
|
||||||
|
const {contentEl} = this;
|
||||||
|
contentEl.setText('Woah!');
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose() {
|
||||||
|
const {contentEl} = this;
|
||||||
|
contentEl.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SampleSettingTab extends PluginSettingTab {
|
||||||
|
plugin: MyPlugin;
|
||||||
|
|
||||||
|
constructor(app: App, plugin: MyPlugin) {
|
||||||
|
super(app, plugin);
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
display(): void {
|
||||||
|
const {containerEl} = this;
|
||||||
|
|
||||||
|
containerEl.empty();
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Setting #1')
|
||||||
|
.setDesc('It\'s a secret')
|
||||||
|
.addText(text => text
|
||||||
|
.setPlaceholder('Enter your secret')
|
||||||
|
.setValue(this.plugin.settings.mySetting)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.mySetting = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
11
test-project/manifest.json
Normal file
11
test-project/manifest.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"id": "sample-plugin",
|
||||||
|
"name": "Sample Plugin",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"minAppVersion": "0.15.0",
|
||||||
|
"description": "Demonstrates some of the capabilities of the Obsidian API.",
|
||||||
|
"author": "Obsidian",
|
||||||
|
"authorUrl": "https://obsidian.md",
|
||||||
|
"fundingUrl": "https://obsidian.md/pricing",
|
||||||
|
"isDesktopOnly": false
|
||||||
|
}
|
29
test-project/package.json
Normal file
29
test-project/package.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "obsidian-sample-plugin",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
|
||||||
|
"main": "main.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "node esbuild.config.mjs",
|
||||||
|
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||||
|
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"obsidian-testing-framework": "workspace:^"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.48.1",
|
||||||
|
"@types/node": "^16.11.6",
|
||||||
|
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||||
|
"@typescript-eslint/parser": "5.29.0",
|
||||||
|
"builtin-modules": "3.3.0",
|
||||||
|
"esbuild": "0.17.3",
|
||||||
|
"obsidian": "latest",
|
||||||
|
"tslib": "2.4.0",
|
||||||
|
"typescript": "4.7.4"
|
||||||
|
}
|
||||||
|
}
|
80
test-project/playwright.config.ts
Normal file
80
test-project/playwright.config.ts
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read environment variables from file.
|
||||||
|
* https://github.com/motdotla/dotenv
|
||||||
|
*/
|
||||||
|
// import dotenv from 'dotenv';
|
||||||
|
// import path from 'path';
|
||||||
|
// dotenv.config({ path: path.resolve(__dirname, '.env') });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See https://playwright.dev/docs/test-configuration.
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './tests',
|
||||||
|
|
||||||
|
/* Run tests in files in parallel */
|
||||||
|
fullyParallel: true,
|
||||||
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
/* Retry on CI only */
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
/* Opt out of parallel tests on CI. */
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
|
reporter: 'html',
|
||||||
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
|
use: {
|
||||||
|
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||||
|
// baseURL: 'http://127.0.0.1:3000',
|
||||||
|
|
||||||
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Configure projects for major browsers */
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: { ...devices['Desktop Chrome'] },
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'firefox',
|
||||||
|
use: { ...devices['Desktop Firefox'] },
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'webkit',
|
||||||
|
use: { ...devices['Desktop Safari'] },
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Test against mobile viewports. */
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Chrome',
|
||||||
|
// use: { ...devices['Pixel 5'] },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Safari',
|
||||||
|
// use: { ...devices['iPhone 12'] },
|
||||||
|
// },
|
||||||
|
|
||||||
|
/* Test against branded browsers. */
|
||||||
|
// {
|
||||||
|
// name: 'Microsoft Edge',
|
||||||
|
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Google Chrome',
|
||||||
|
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
|
||||||
|
/* Run your local dev server before starting the tests */
|
||||||
|
// webServer: {
|
||||||
|
// command: 'npm run start',
|
||||||
|
// url: 'http://127.0.0.1:3000',
|
||||||
|
// reuseExistingServer: !process.env.CI,
|
||||||
|
// },
|
||||||
|
});
|
437
test-project/tests-examples/demo-todo-app.spec.ts
Normal file
437
test-project/tests-examples/demo-todo-app.spec.ts
Normal file
@ -0,0 +1,437 @@
|
|||||||
|
import { test, expect, type Page } from '@playwright/test';
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('https://demo.playwright.dev/todomvc');
|
||||||
|
});
|
||||||
|
|
||||||
|
const TODO_ITEMS = [
|
||||||
|
'buy some cheese',
|
||||||
|
'feed the cat',
|
||||||
|
'book a doctors appointment'
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
test.describe('New Todo', () => {
|
||||||
|
test('should allow me to add todo items', async ({ page }) => {
|
||||||
|
// create a new todo locator
|
||||||
|
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||||
|
|
||||||
|
// Create 1st todo.
|
||||||
|
await newTodo.fill(TODO_ITEMS[0]);
|
||||||
|
await newTodo.press('Enter');
|
||||||
|
|
||||||
|
// Make sure the list only has one todo item.
|
||||||
|
await expect(page.getByTestId('todo-title')).toHaveText([
|
||||||
|
TODO_ITEMS[0]
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create 2nd todo.
|
||||||
|
await newTodo.fill(TODO_ITEMS[1]);
|
||||||
|
await newTodo.press('Enter');
|
||||||
|
|
||||||
|
// Make sure the list now has two todo items.
|
||||||
|
await expect(page.getByTestId('todo-title')).toHaveText([
|
||||||
|
TODO_ITEMS[0],
|
||||||
|
TODO_ITEMS[1]
|
||||||
|
]);
|
||||||
|
|
||||||
|
await checkNumberOfTodosInLocalStorage(page, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should clear text input field when an item is added', async ({ page }) => {
|
||||||
|
// create a new todo locator
|
||||||
|
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||||
|
|
||||||
|
// Create one todo item.
|
||||||
|
await newTodo.fill(TODO_ITEMS[0]);
|
||||||
|
await newTodo.press('Enter');
|
||||||
|
|
||||||
|
// Check that input is empty.
|
||||||
|
await expect(newTodo).toBeEmpty();
|
||||||
|
await checkNumberOfTodosInLocalStorage(page, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should append new items to the bottom of the list', async ({ page }) => {
|
||||||
|
// Create 3 items.
|
||||||
|
await createDefaultTodos(page);
|
||||||
|
|
||||||
|
// create a todo count locator
|
||||||
|
const todoCount = page.getByTestId('todo-count')
|
||||||
|
|
||||||
|
// Check test using different methods.
|
||||||
|
await expect(page.getByText('3 items left')).toBeVisible();
|
||||||
|
await expect(todoCount).toHaveText('3 items left');
|
||||||
|
await expect(todoCount).toContainText('3');
|
||||||
|
await expect(todoCount).toHaveText(/3/);
|
||||||
|
|
||||||
|
// Check all items in one call.
|
||||||
|
await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS);
|
||||||
|
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Mark all as completed', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await createDefaultTodos(page);
|
||||||
|
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterEach(async ({ page }) => {
|
||||||
|
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow me to mark all items as completed', async ({ page }) => {
|
||||||
|
// Complete all todos.
|
||||||
|
await page.getByLabel('Mark all as complete').check();
|
||||||
|
|
||||||
|
// Ensure all todos have 'completed' class.
|
||||||
|
await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']);
|
||||||
|
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow me to clear the complete state of all items', async ({ page }) => {
|
||||||
|
const toggleAll = page.getByLabel('Mark all as complete');
|
||||||
|
// Check and then immediately uncheck.
|
||||||
|
await toggleAll.check();
|
||||||
|
await toggleAll.uncheck();
|
||||||
|
|
||||||
|
// Should be no completed classes.
|
||||||
|
await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => {
|
||||||
|
const toggleAll = page.getByLabel('Mark all as complete');
|
||||||
|
await toggleAll.check();
|
||||||
|
await expect(toggleAll).toBeChecked();
|
||||||
|
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
|
||||||
|
|
||||||
|
// Uncheck first todo.
|
||||||
|
const firstTodo = page.getByTestId('todo-item').nth(0);
|
||||||
|
await firstTodo.getByRole('checkbox').uncheck();
|
||||||
|
|
||||||
|
// Reuse toggleAll locator and make sure its not checked.
|
||||||
|
await expect(toggleAll).not.toBeChecked();
|
||||||
|
|
||||||
|
await firstTodo.getByRole('checkbox').check();
|
||||||
|
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
|
||||||
|
|
||||||
|
// Assert the toggle all is checked again.
|
||||||
|
await expect(toggleAll).toBeChecked();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Item', () => {
|
||||||
|
|
||||||
|
test('should allow me to mark items as complete', async ({ page }) => {
|
||||||
|
// create a new todo locator
|
||||||
|
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||||
|
|
||||||
|
// Create two items.
|
||||||
|
for (const item of TODO_ITEMS.slice(0, 2)) {
|
||||||
|
await newTodo.fill(item);
|
||||||
|
await newTodo.press('Enter');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check first item.
|
||||||
|
const firstTodo = page.getByTestId('todo-item').nth(0);
|
||||||
|
await firstTodo.getByRole('checkbox').check();
|
||||||
|
await expect(firstTodo).toHaveClass('completed');
|
||||||
|
|
||||||
|
// Check second item.
|
||||||
|
const secondTodo = page.getByTestId('todo-item').nth(1);
|
||||||
|
await expect(secondTodo).not.toHaveClass('completed');
|
||||||
|
await secondTodo.getByRole('checkbox').check();
|
||||||
|
|
||||||
|
// Assert completed class.
|
||||||
|
await expect(firstTodo).toHaveClass('completed');
|
||||||
|
await expect(secondTodo).toHaveClass('completed');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow me to un-mark items as complete', async ({ page }) => {
|
||||||
|
// create a new todo locator
|
||||||
|
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||||
|
|
||||||
|
// Create two items.
|
||||||
|
for (const item of TODO_ITEMS.slice(0, 2)) {
|
||||||
|
await newTodo.fill(item);
|
||||||
|
await newTodo.press('Enter');
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstTodo = page.getByTestId('todo-item').nth(0);
|
||||||
|
const secondTodo = page.getByTestId('todo-item').nth(1);
|
||||||
|
const firstTodoCheckbox = firstTodo.getByRole('checkbox');
|
||||||
|
|
||||||
|
await firstTodoCheckbox.check();
|
||||||
|
await expect(firstTodo).toHaveClass('completed');
|
||||||
|
await expect(secondTodo).not.toHaveClass('completed');
|
||||||
|
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||||
|
|
||||||
|
await firstTodoCheckbox.uncheck();
|
||||||
|
await expect(firstTodo).not.toHaveClass('completed');
|
||||||
|
await expect(secondTodo).not.toHaveClass('completed');
|
||||||
|
await checkNumberOfCompletedTodosInLocalStorage(page, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow me to edit an item', async ({ page }) => {
|
||||||
|
await createDefaultTodos(page);
|
||||||
|
|
||||||
|
const todoItems = page.getByTestId('todo-item');
|
||||||
|
const secondTodo = todoItems.nth(1);
|
||||||
|
await secondTodo.dblclick();
|
||||||
|
await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]);
|
||||||
|
await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
|
||||||
|
await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter');
|
||||||
|
|
||||||
|
// Explicitly assert the new text value.
|
||||||
|
await expect(todoItems).toHaveText([
|
||||||
|
TODO_ITEMS[0],
|
||||||
|
'buy some sausages',
|
||||||
|
TODO_ITEMS[2]
|
||||||
|
]);
|
||||||
|
await checkTodosInLocalStorage(page, 'buy some sausages');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Editing', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await createDefaultTodos(page);
|
||||||
|
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should hide other controls when editing', async ({ page }) => {
|
||||||
|
const todoItem = page.getByTestId('todo-item').nth(1);
|
||||||
|
await todoItem.dblclick();
|
||||||
|
await expect(todoItem.getByRole('checkbox')).not.toBeVisible();
|
||||||
|
await expect(todoItem.locator('label', {
|
||||||
|
hasText: TODO_ITEMS[1],
|
||||||
|
})).not.toBeVisible();
|
||||||
|
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should save edits on blur', async ({ page }) => {
|
||||||
|
const todoItems = page.getByTestId('todo-item');
|
||||||
|
await todoItems.nth(1).dblclick();
|
||||||
|
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
|
||||||
|
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur');
|
||||||
|
|
||||||
|
await expect(todoItems).toHaveText([
|
||||||
|
TODO_ITEMS[0],
|
||||||
|
'buy some sausages',
|
||||||
|
TODO_ITEMS[2],
|
||||||
|
]);
|
||||||
|
await checkTodosInLocalStorage(page, 'buy some sausages');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should trim entered text', async ({ page }) => {
|
||||||
|
const todoItems = page.getByTestId('todo-item');
|
||||||
|
await todoItems.nth(1).dblclick();
|
||||||
|
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages ');
|
||||||
|
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter');
|
||||||
|
|
||||||
|
await expect(todoItems).toHaveText([
|
||||||
|
TODO_ITEMS[0],
|
||||||
|
'buy some sausages',
|
||||||
|
TODO_ITEMS[2],
|
||||||
|
]);
|
||||||
|
await checkTodosInLocalStorage(page, 'buy some sausages');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should remove the item if an empty text string was entered', async ({ page }) => {
|
||||||
|
const todoItems = page.getByTestId('todo-item');
|
||||||
|
await todoItems.nth(1).dblclick();
|
||||||
|
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('');
|
||||||
|
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter');
|
||||||
|
|
||||||
|
await expect(todoItems).toHaveText([
|
||||||
|
TODO_ITEMS[0],
|
||||||
|
TODO_ITEMS[2],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should cancel edits on escape', async ({ page }) => {
|
||||||
|
const todoItems = page.getByTestId('todo-item');
|
||||||
|
await todoItems.nth(1).dblclick();
|
||||||
|
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
|
||||||
|
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape');
|
||||||
|
await expect(todoItems).toHaveText(TODO_ITEMS);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Counter', () => {
|
||||||
|
test('should display the current number of todo items', async ({ page }) => {
|
||||||
|
// create a new todo locator
|
||||||
|
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||||
|
|
||||||
|
// create a todo count locator
|
||||||
|
const todoCount = page.getByTestId('todo-count')
|
||||||
|
|
||||||
|
await newTodo.fill(TODO_ITEMS[0]);
|
||||||
|
await newTodo.press('Enter');
|
||||||
|
|
||||||
|
await expect(todoCount).toContainText('1');
|
||||||
|
|
||||||
|
await newTodo.fill(TODO_ITEMS[1]);
|
||||||
|
await newTodo.press('Enter');
|
||||||
|
await expect(todoCount).toContainText('2');
|
||||||
|
|
||||||
|
await checkNumberOfTodosInLocalStorage(page, 2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Clear completed button', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await createDefaultTodos(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display the correct text', async ({ page }) => {
|
||||||
|
await page.locator('.todo-list li .toggle').first().check();
|
||||||
|
await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should remove completed items when clicked', async ({ page }) => {
|
||||||
|
const todoItems = page.getByTestId('todo-item');
|
||||||
|
await todoItems.nth(1).getByRole('checkbox').check();
|
||||||
|
await page.getByRole('button', { name: 'Clear completed' }).click();
|
||||||
|
await expect(todoItems).toHaveCount(2);
|
||||||
|
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should be hidden when there are no items that are completed', async ({ page }) => {
|
||||||
|
await page.locator('.todo-list li .toggle').first().check();
|
||||||
|
await page.getByRole('button', { name: 'Clear completed' }).click();
|
||||||
|
await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Persistence', () => {
|
||||||
|
test('should persist its data', async ({ page }) => {
|
||||||
|
// create a new todo locator
|
||||||
|
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||||
|
|
||||||
|
for (const item of TODO_ITEMS.slice(0, 2)) {
|
||||||
|
await newTodo.fill(item);
|
||||||
|
await newTodo.press('Enter');
|
||||||
|
}
|
||||||
|
|
||||||
|
const todoItems = page.getByTestId('todo-item');
|
||||||
|
const firstTodoCheck = todoItems.nth(0).getByRole('checkbox');
|
||||||
|
await firstTodoCheck.check();
|
||||||
|
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
|
||||||
|
await expect(firstTodoCheck).toBeChecked();
|
||||||
|
await expect(todoItems).toHaveClass(['completed', '']);
|
||||||
|
|
||||||
|
// Ensure there is 1 completed item.
|
||||||
|
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||||
|
|
||||||
|
// Now reload.
|
||||||
|
await page.reload();
|
||||||
|
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
|
||||||
|
await expect(firstTodoCheck).toBeChecked();
|
||||||
|
await expect(todoItems).toHaveClass(['completed', '']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Routing', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await createDefaultTodos(page);
|
||||||
|
// make sure the app had a chance to save updated todos in storage
|
||||||
|
// before navigating to a new view, otherwise the items can get lost :(
|
||||||
|
// in some frameworks like Durandal
|
||||||
|
await checkTodosInLocalStorage(page, TODO_ITEMS[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow me to display active items', async ({ page }) => {
|
||||||
|
const todoItem = page.getByTestId('todo-item');
|
||||||
|
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
|
||||||
|
|
||||||
|
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||||
|
await page.getByRole('link', { name: 'Active' }).click();
|
||||||
|
await expect(todoItem).toHaveCount(2);
|
||||||
|
await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should respect the back button', async ({ page }) => {
|
||||||
|
const todoItem = page.getByTestId('todo-item');
|
||||||
|
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
|
||||||
|
|
||||||
|
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||||
|
|
||||||
|
await test.step('Showing all items', async () => {
|
||||||
|
await page.getByRole('link', { name: 'All' }).click();
|
||||||
|
await expect(todoItem).toHaveCount(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Showing active items', async () => {
|
||||||
|
await page.getByRole('link', { name: 'Active' }).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Showing completed items', async () => {
|
||||||
|
await page.getByRole('link', { name: 'Completed' }).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(todoItem).toHaveCount(1);
|
||||||
|
await page.goBack();
|
||||||
|
await expect(todoItem).toHaveCount(2);
|
||||||
|
await page.goBack();
|
||||||
|
await expect(todoItem).toHaveCount(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow me to display completed items', async ({ page }) => {
|
||||||
|
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
|
||||||
|
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||||
|
await page.getByRole('link', { name: 'Completed' }).click();
|
||||||
|
await expect(page.getByTestId('todo-item')).toHaveCount(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow me to display all items', async ({ page }) => {
|
||||||
|
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
|
||||||
|
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||||
|
await page.getByRole('link', { name: 'Active' }).click();
|
||||||
|
await page.getByRole('link', { name: 'Completed' }).click();
|
||||||
|
await page.getByRole('link', { name: 'All' }).click();
|
||||||
|
await expect(page.getByTestId('todo-item')).toHaveCount(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should highlight the currently applied filter', async ({ page }) => {
|
||||||
|
await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected');
|
||||||
|
|
||||||
|
//create locators for active and completed links
|
||||||
|
const activeLink = page.getByRole('link', { name: 'Active' });
|
||||||
|
const completedLink = page.getByRole('link', { name: 'Completed' });
|
||||||
|
await activeLink.click();
|
||||||
|
|
||||||
|
// Page change - active items.
|
||||||
|
await expect(activeLink).toHaveClass('selected');
|
||||||
|
await completedLink.click();
|
||||||
|
|
||||||
|
// Page change - completed items.
|
||||||
|
await expect(completedLink).toHaveClass('selected');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function createDefaultTodos(page: Page) {
|
||||||
|
// create a new todo locator
|
||||||
|
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||||
|
|
||||||
|
for (const item of TODO_ITEMS) {
|
||||||
|
await newTodo.fill(item);
|
||||||
|
await newTodo.press('Enter');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) {
|
||||||
|
return await page.waitForFunction(e => {
|
||||||
|
return JSON.parse(localStorage['react-todos']).length === e;
|
||||||
|
}, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) {
|
||||||
|
return await page.waitForFunction(e => {
|
||||||
|
return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e;
|
||||||
|
}, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkTodosInLocalStorage(page: Page, title: string) {
|
||||||
|
return await page.waitForFunction(t => {
|
||||||
|
return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t);
|
||||||
|
}, title);
|
||||||
|
}
|
9
test-project/tests/example.spec.ts
Normal file
9
test-project/tests/example.spec.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { expect } from '@playwright/test';
|
||||||
|
import {test} from "obsidian-testing-framework"
|
||||||
|
test('something', async ({ page }) => {
|
||||||
|
expect(page).toHaveURL(/obsidian\.md/i);
|
||||||
|
});
|
||||||
|
test("idk", async({app}) => {
|
||||||
|
let thing = app.metadataCache.getFirstLinkpathDest("Welcome", ".");
|
||||||
|
expect(thing?.basename).toEqual("Welcome");
|
||||||
|
})
|
24
test-project/tsconfig.json
Normal file
24
test-project/tsconfig.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"inlineSourceMap": true,
|
||||||
|
"inlineSources": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"target": "ES6",
|
||||||
|
"allowJs": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"importHelpers": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"lib": [
|
||||||
|
"DOM",
|
||||||
|
"ES5",
|
||||||
|
"ES6",
|
||||||
|
"ES7"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"**/*.ts"
|
||||||
|
]
|
||||||
|
}
|
14
test-project/version-bump.mjs
Normal file
14
test-project/version-bump.mjs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { readFileSync, writeFileSync } from "fs";
|
||||||
|
|
||||||
|
const targetVersion = process.env.npm_package_version;
|
||||||
|
|
||||||
|
// read minAppVersion from manifest.json and bump version to target version
|
||||||
|
let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
|
||||||
|
const { minAppVersion } = manifest;
|
||||||
|
manifest.version = targetVersion;
|
||||||
|
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
|
||||||
|
|
||||||
|
// update versions.json with target version and minAppVersion from manifest.json
|
||||||
|
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
|
||||||
|
versions[targetVersion] = minAppVersion;
|
||||||
|
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));
|
3
test-project/versions.json
Normal file
3
test-project/versions.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"1.0.0": "0.15.0"
|
||||||
|
}
|
39
tsconfig.json
Normal file
39
tsconfig.json
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"lib": [
|
||||||
|
"es2015",
|
||||||
|
],
|
||||||
|
"outDir": "lib",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"strict": true,
|
||||||
|
"alwaysStrict": true,
|
||||||
|
"strictFunctionTypes": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"strictPropertyInitialization": false,
|
||||||
|
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"downlevelIteration": true,
|
||||||
|
"declaration": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"pretty": true,
|
||||||
|
"types": ["node"],
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"typings/**/*",
|
||||||
|
"src/**/*"
|
||||||
|
],
|
||||||
|
"exclude": ["test-project/**/*"]
|
||||||
|
}
|
5
typings/global.d.ts
vendored
Normal file
5
typings/global.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import {App} from "obsidian";
|
||||||
|
declare global {
|
||||||
|
// const app: App;
|
||||||
|
}
|
||||||
|
export {}
|
1
vault/test-framework-tester/.obsidian/app.json
vendored
Normal file
1
vault/test-framework-tester/.obsidian/app.json
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{}
|
1
vault/test-framework-tester/.obsidian/appearance.json
vendored
Normal file
1
vault/test-framework-tester/.obsidian/appearance.json
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{}
|
30
vault/test-framework-tester/.obsidian/core-plugins.json
vendored
Normal file
30
vault/test-framework-tester/.obsidian/core-plugins.json
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"file-explorer": true,
|
||||||
|
"global-search": true,
|
||||||
|
"switcher": true,
|
||||||
|
"graph": true,
|
||||||
|
"backlink": true,
|
||||||
|
"canvas": true,
|
||||||
|
"outgoing-link": true,
|
||||||
|
"tag-pane": true,
|
||||||
|
"properties": false,
|
||||||
|
"page-preview": true,
|
||||||
|
"daily-notes": true,
|
||||||
|
"templates": true,
|
||||||
|
"note-composer": true,
|
||||||
|
"command-palette": true,
|
||||||
|
"slash-command": false,
|
||||||
|
"editor-status": true,
|
||||||
|
"bookmarks": true,
|
||||||
|
"markdown-importer": false,
|
||||||
|
"zk-prefixer": false,
|
||||||
|
"random-note": false,
|
||||||
|
"outline": true,
|
||||||
|
"word-count": true,
|
||||||
|
"slides": false,
|
||||||
|
"audio-recorder": false,
|
||||||
|
"workspaces": false,
|
||||||
|
"file-recovery": true,
|
||||||
|
"publish": false,
|
||||||
|
"sync": false
|
||||||
|
}
|
22
vault/test-framework-tester/.obsidian/graph.json
vendored
Normal file
22
vault/test-framework-tester/.obsidian/graph.json
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"collapse-filter": true,
|
||||||
|
"search": "",
|
||||||
|
"showTags": false,
|
||||||
|
"showAttachments": false,
|
||||||
|
"hideUnresolved": false,
|
||||||
|
"showOrphans": true,
|
||||||
|
"collapse-color-groups": true,
|
||||||
|
"colorGroups": [],
|
||||||
|
"collapse-display": true,
|
||||||
|
"showArrow": false,
|
||||||
|
"textFadeMultiplier": 0,
|
||||||
|
"nodeSizeMultiplier": 1,
|
||||||
|
"lineSizeMultiplier": 1,
|
||||||
|
"collapse-forces": true,
|
||||||
|
"centerStrength": 0.518713248970312,
|
||||||
|
"repelStrength": 10,
|
||||||
|
"linkStrength": 1,
|
||||||
|
"linkDistance": 250,
|
||||||
|
"scale": 1,
|
||||||
|
"close": true
|
||||||
|
}
|
185
vault/test-framework-tester/.obsidian/workspace.json
vendored
Normal file
185
vault/test-framework-tester/.obsidian/workspace.json
vendored
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
{
|
||||||
|
"main": {
|
||||||
|
"id": "c2fe4c323d36fc5e",
|
||||||
|
"type": "split",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "8298a75d0a6318ad",
|
||||||
|
"type": "tabs",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "c166e51cf23ccba4",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "markdown",
|
||||||
|
"state": {
|
||||||
|
"file": "Welcome.md",
|
||||||
|
"mode": "source",
|
||||||
|
"source": false
|
||||||
|
},
|
||||||
|
"icon": "lucide-file",
|
||||||
|
"title": "Welcome"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ba8373549c03c775",
|
||||||
|
"type": "tabs",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "95e0d337b44eab37",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "graph",
|
||||||
|
"state": {},
|
||||||
|
"icon": "lucide-git-fork",
|
||||||
|
"title": "Graph view"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"direction": "vertical"
|
||||||
|
},
|
||||||
|
"left": {
|
||||||
|
"id": "15ee0403a7a614b4",
|
||||||
|
"type": "split",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "f297c8bc39873aa8",
|
||||||
|
"type": "tabs",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "cee48933474ae820",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "file-explorer",
|
||||||
|
"state": {
|
||||||
|
"sortOrder": "alphabetical"
|
||||||
|
},
|
||||||
|
"icon": "lucide-folder-closed",
|
||||||
|
"title": "Files"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "a5eb514f5fbe0a9d",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "search",
|
||||||
|
"state": {
|
||||||
|
"query": "",
|
||||||
|
"matchingCase": false,
|
||||||
|
"explainSearch": false,
|
||||||
|
"collapseAll": false,
|
||||||
|
"extraContext": false,
|
||||||
|
"sortOrder": "alphabetical"
|
||||||
|
},
|
||||||
|
"icon": "lucide-search",
|
||||||
|
"title": "Search"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2ea1b4740307baf6",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "bookmarks",
|
||||||
|
"state": {},
|
||||||
|
"icon": "lucide-bookmark",
|
||||||
|
"title": "Bookmarks"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"direction": "horizontal",
|
||||||
|
"width": 300
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"id": "9da40e5b7fceeacd",
|
||||||
|
"type": "split",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "2e127fb9abc8dd11",
|
||||||
|
"type": "tabs",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "21b5909f77e5cae0",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "backlink",
|
||||||
|
"state": {
|
||||||
|
"file": "Welcome.md",
|
||||||
|
"collapseAll": false,
|
||||||
|
"extraContext": false,
|
||||||
|
"sortOrder": "alphabetical",
|
||||||
|
"showSearch": false,
|
||||||
|
"searchQuery": "",
|
||||||
|
"backlinkCollapsed": false,
|
||||||
|
"unlinkedCollapsed": true
|
||||||
|
},
|
||||||
|
"icon": "links-coming-in",
|
||||||
|
"title": "Backlinks for Welcome"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bc9d35775b8807b0",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "outgoing-link",
|
||||||
|
"state": {
|
||||||
|
"file": "Welcome.md",
|
||||||
|
"linksCollapsed": false,
|
||||||
|
"unlinkedCollapsed": true
|
||||||
|
},
|
||||||
|
"icon": "links-going-out",
|
||||||
|
"title": "Outgoing links from Welcome"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "982543d44de11473",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "tag",
|
||||||
|
"state": {
|
||||||
|
"sortOrder": "frequency",
|
||||||
|
"useHierarchy": true
|
||||||
|
},
|
||||||
|
"icon": "lucide-tags",
|
||||||
|
"title": "Tags"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "52f3666280022d63",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "outline",
|
||||||
|
"state": {
|
||||||
|
"file": "Welcome.md"
|
||||||
|
},
|
||||||
|
"icon": "lucide-list",
|
||||||
|
"title": "Outline of Welcome"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"direction": "horizontal",
|
||||||
|
"width": 300,
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
"left-ribbon": {
|
||||||
|
"hiddenItems": {
|
||||||
|
"switcher:Open quick switcher": false,
|
||||||
|
"graph:Open graph view": false,
|
||||||
|
"canvas:Create new canvas": false,
|
||||||
|
"daily-notes:Open today's daily note": false,
|
||||||
|
"templates:Insert template": false,
|
||||||
|
"command-palette:Open command palette": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"active": "c166e51cf23ccba4",
|
||||||
|
"lastOpenFiles": [
|
||||||
|
"Welcome.md"
|
||||||
|
]
|
||||||
|
}
|
5
vault/test-framework-tester/Welcome.md
Normal file
5
vault/test-framework-tester/Welcome.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
This is your new *vault*.
|
||||||
|
|
||||||
|
Make a note of something, [[create a link]], or try [the Importer](https://help.obsidian.md/Plugins/Importer)!
|
||||||
|
|
||||||
|
When you're ready, delete this note and make the vault your own.
|
Loading…
Reference in New Issue
Block a user