This commit is contained in:
commit
8c645a73b3
14
.editorconfig
Normal file
14
.editorconfig
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.{js,json,ts,tsx,jsx}]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.yml]
|
||||||
|
indent_style = space
|
||||||
|
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
|
38
.github/workflows/playwright.yml
vendored
Normal file
38
.github/workflows/playwright.yml
vendored
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
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: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y jq xdg-utils xvfb
|
||||||
|
curled=$(curl -L https://github.com/obsidianmd/obsidian-releases/raw/refs/heads/master/desktop-releases.json | jq .latestVersion | sed s/\"//g)
|
||||||
|
curl -Lo obsidian.deb "https://github.com/obsidianmd/obsidian-releases/releases/download/v$curled/obsidian_${curled}_amd64.deb"
|
||||||
|
corepack enable
|
||||||
|
yarn install
|
||||||
|
cd packages/test-project
|
||||||
|
yarn playwright install-deps
|
||||||
|
dpkg -i ../../obsidian.deb
|
||||||
|
- name: compile and test!
|
||||||
|
env:
|
||||||
|
XDG_CONFIG_HOME: /root/.config
|
||||||
|
run: |
|
||||||
|
yarn workspace obsidian-testing-framework run tsc
|
||||||
|
yarn workspace obsidian-sample-plugin run test
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: playwright-report/
|
||||||
|
retention-days: 30
|
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
test-results/
|
||||||
|
playwright-report/
|
||||||
|
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
|
8
package.json
Normal file
8
package.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "obsidian-testing-framework-parent",
|
||||||
|
"private": true,
|
||||||
|
"packageManager": "yarn@4.5.1",
|
||||||
|
"workspaces": [
|
||||||
|
"./packages/*"
|
||||||
|
]
|
||||||
|
}
|
43
packages/obsidian-testing-framework/package.json
Normal file
43
packages/obsidian-testing-framework/package.json
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "obsidian-testing-framework",
|
||||||
|
"packageManager": "yarn@4.5.1",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/language": "https://github.com/lishid/cm-language",
|
||||||
|
"@codemirror/state": "^6.0.1",
|
||||||
|
"@codemirror/view": "^6.0.1",
|
||||||
|
"asar": "^3.2.0",
|
||||||
|
"electron": "^33.0.2",
|
||||||
|
"playwright": "^1.48.1",
|
||||||
|
"tmp": "^0.2.3",
|
||||||
|
"typescript": "^5.6.3"
|
||||||
|
},
|
||||||
|
"version": "",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./lib/index.d.ts",
|
||||||
|
"default": "./lib/index.js"
|
||||||
|
},
|
||||||
|
"./utils": {
|
||||||
|
"types": "./lib/util.d.ts",
|
||||||
|
"default": "./lib/util.js"
|
||||||
|
},
|
||||||
|
"./fixture": {
|
||||||
|
"types": "./lib/fixtures.d.ts",
|
||||||
|
"default": "./lib/fixtures.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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/tmp": "^0",
|
||||||
|
"obsidian": "latest",
|
||||||
|
"vitest": "^2.1.3"
|
||||||
|
},
|
||||||
|
"type": "module"
|
||||||
|
}
|
11
packages/obsidian-testing-framework/src/fixtures.ts
Normal file
11
packages/obsidian-testing-framework/src/fixtures.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { ElectronApplication, JSHandle, Page } from "playwright";
|
||||||
|
import { ObsidianTestingConfig } from "./index.js";
|
||||||
|
import { App } from "obsidian";
|
||||||
|
// import { getFile } from "./util.js";
|
||||||
|
|
||||||
|
export interface ObsidianTestFixtures {
|
||||||
|
electronApp: ElectronApplication;
|
||||||
|
page: Page;
|
||||||
|
obsidian: ObsidianTestingConfig;
|
||||||
|
appHandle: JSHandle<App>;
|
||||||
|
}
|
138
packages/obsidian-testing-framework/src/index.ts
Normal file
138
packages/obsidian-testing-framework/src/index.ts
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import { test as base } from "@playwright/test";
|
||||||
|
import { _electron as electron } from "playwright";
|
||||||
|
import { Fixtures } from "@playwright/test";
|
||||||
|
import path from "path";
|
||||||
|
import { ObsidianTestFixtures } from "./fixtures.js";
|
||||||
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
||||||
|
import { getApp, waitForIndexingComplete } from "./util.js";
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
import { randomBytes } from "crypto";
|
||||||
|
|
||||||
|
export interface ObsidianTestingConfig {
|
||||||
|
vault?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getExe(): string {
|
||||||
|
checkToy();
|
||||||
|
if (process.platform == "win32") {
|
||||||
|
return path.join(
|
||||||
|
process.env.LOCALAPPDATA!,
|
||||||
|
"Obsidian",
|
||||||
|
"Resources",
|
||||||
|
"app.asar"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const possibleDirs = [
|
||||||
|
"/opt/Obsidian",
|
||||||
|
"/usr/lib/Obsidian",
|
||||||
|
"/opt/obsidian",
|
||||||
|
"/usr/lib/obsidian",
|
||||||
|
"/var/lib/flatpak/app/md.obsidian.Obsidian/current/active/files",
|
||||||
|
"/snap/obsidian/current",
|
||||||
|
];
|
||||||
|
for (let i = 0; i < possibleDirs.length; i++) {
|
||||||
|
if (existsSync(possibleDirs[i])) {
|
||||||
|
console.log(execSync(`ls -l ${possibleDirs[i]}`).toString());
|
||||||
|
return path.join(possibleDirs[i], "resources", "app.asar");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkToy() {
|
||||||
|
if (process.platform == "darwin") {
|
||||||
|
throw new Error("use a non-toy operating system, dumbass");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateVaultConfig(vault: string) {
|
||||||
|
const vaultHash = randomBytes(16).toString("hex").toLocaleLowerCase();
|
||||||
|
let configLocation;
|
||||||
|
checkToy();
|
||||||
|
if (process.platform == "win32") {
|
||||||
|
configLocation = path.join(`${process.env.LOCALAPPDATA}`, "Obsidian");
|
||||||
|
} else {
|
||||||
|
configLocation = path.join(`${process.env.XDG_CONFIG_HOME}`, "obsidian");
|
||||||
|
try {
|
||||||
|
mkdirSync(configLocation, { recursive: true });
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
const obsidianConfigFile = path.join(configLocation, "obsidian.json");
|
||||||
|
if (!existsSync(obsidianConfigFile)) {
|
||||||
|
writeFileSync(obsidianConfigFile, JSON.stringify({ vaults: {} }));
|
||||||
|
}
|
||||||
|
const json: {
|
||||||
|
vaults: {
|
||||||
|
[key: string]: {
|
||||||
|
path: string;
|
||||||
|
ts: number;
|
||||||
|
open?: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
} = JSON.parse(readFileSync(obsidianConfigFile).toString());
|
||||||
|
|
||||||
|
if (!Object.values(json.vaults).some((a) => a.path === vault)) {
|
||||||
|
json.vaults[vaultHash] = {
|
||||||
|
path: vault,
|
||||||
|
ts: Date.now(),
|
||||||
|
};
|
||||||
|
writeFileSync(obsidianConfigFile, JSON.stringify(json));
|
||||||
|
writeFileSync(path.join(configLocation, `${vaultHash}.json`), "{}");
|
||||||
|
return vaultHash;
|
||||||
|
} else {
|
||||||
|
return Object.entries(json.vaults).find(a => a[1].path === vault)![0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const obsidianTestFixtures: Fixtures<ObsidianTestFixtures> = {
|
||||||
|
electronApp: [
|
||||||
|
async ({ obsidian: { vault } }, run) => {
|
||||||
|
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = "true";
|
||||||
|
console.log("asar located at:", getExe());
|
||||||
|
let uriArg = "";
|
||||||
|
if (vault) {
|
||||||
|
let id = generateVaultConfig(vault);
|
||||||
|
if (!!id) {
|
||||||
|
uriArg = `obsidian://open?vault=${encodeURIComponent(id)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const electronApp = await electron.launch({
|
||||||
|
timeout: 60000,
|
||||||
|
args: [getExe(), uriArg].filter((a) => !!a) as string[],
|
||||||
|
});
|
||||||
|
electronApp.on("console", async (msg) => {
|
||||||
|
console.log(
|
||||||
|
...(await Promise.all(msg.args().map((a) => a.jsonValue())))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await electronApp.waitForEvent("window");
|
||||||
|
await run(electronApp);
|
||||||
|
await electronApp.close();
|
||||||
|
},
|
||||||
|
{ timeout: 60000 },
|
||||||
|
],
|
||||||
|
page: [
|
||||||
|
async ({ electronApp }, run) => {
|
||||||
|
const windows = electronApp.windows();
|
||||||
|
// console.log("windows", windows);
|
||||||
|
let page = windows[windows.length - 1]!;
|
||||||
|
await page.waitForLoadState("domcontentloaded");
|
||||||
|
page = electronApp.windows()[electronApp.windows().length - 1]!;
|
||||||
|
await page.waitForEvent("load");
|
||||||
|
await page.waitForLoadState("domcontentloaded");
|
||||||
|
await waitForIndexingComplete(await getApp(page));
|
||||||
|
page.on("console", async (msg) => {
|
||||||
|
console.log(
|
||||||
|
...(await Promise.all(msg.args().map((a) => a.jsonValue())))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await run(page);
|
||||||
|
},
|
||||||
|
{ timeout: 60000 },
|
||||||
|
],
|
||||||
|
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
packages/obsidian-testing-framework/src/tester.ts.not
Normal file
114
packages/obsidian-testing-framework/src/tester.ts.not
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();
|
||||||
|
}
|
||||||
|
}
|
99
packages/obsidian-testing-framework/src/util.ts
Normal file
99
packages/obsidian-testing-framework/src/util.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import { App, TFile } from "obsidian";
|
||||||
|
import { JSHandle, Page } from "playwright";
|
||||||
|
import { expect } from "@playwright/test";
|
||||||
|
|
||||||
|
// type TestArgs = Parameters<Parameters<typeof test>[2]>[0];
|
||||||
|
|
||||||
|
export async function assertFileEquals(
|
||||||
|
page: Page,
|
||||||
|
path: string,
|
||||||
|
expectedContent: string,
|
||||||
|
cached: boolean = true
|
||||||
|
) {
|
||||||
|
const fileContent = await readFile(page, path, cached);
|
||||||
|
|
||||||
|
expect(fileContent).toEqual(normalizeEOL(expectedContent));
|
||||||
|
}
|
||||||
|
export async function assertLineEquals(
|
||||||
|
page: Page,
|
||||||
|
path: string,
|
||||||
|
lineNumber: number,
|
||||||
|
expectedContent: string,
|
||||||
|
cached: boolean = true
|
||||||
|
) {
|
||||||
|
const fileContent = await readFile(page, path, cached);
|
||||||
|
|
||||||
|
expect(fileContent.split("\n")[lineNumber]).toEqual(
|
||||||
|
normalizeEOL(expectedContent)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export async function getApp(page: Page): Promise<JSHandle<App>> {
|
||||||
|
return await page.evaluateHandle("window.app");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function assertLinesEqual(
|
||||||
|
page: Page,
|
||||||
|
filePath: string,
|
||||||
|
start: number,
|
||||||
|
end: number,
|
||||||
|
expected: string,
|
||||||
|
cached: boolean = true
|
||||||
|
) {
|
||||||
|
const fileContent = await readFile(page, 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");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readFile(
|
||||||
|
app: Page,
|
||||||
|
path: string,
|
||||||
|
cached: boolean = true
|
||||||
|
): Promise<string> {
|
||||||
|
return normalizeEOL(
|
||||||
|
await doWithApp(app, (a) => {
|
||||||
|
const file = getFile(a, path);
|
||||||
|
return cached ? a.vault.cachedRead(file) : a.vault.read(file);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function doWithApp<T = unknown>(
|
||||||
|
page: Page,
|
||||||
|
callback: (a: App) => T | Promise<T>
|
||||||
|
): Promise<T> {
|
||||||
|
const cbStr = callback.toString();
|
||||||
|
return await page.evaluate<T, string>(async (cb) => {
|
||||||
|
const func = new Function(`return (${cb})(window.app)`)
|
||||||
|
return await func();
|
||||||
|
}, cbStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function waitForIndexingComplete(appHandle: JSHandle<App>) {
|
||||||
|
return appHandle.evaluate(() => {
|
||||||
|
return new Promise((res2, rej2) => {
|
||||||
|
let resolved = false;
|
||||||
|
app.metadataCache.on("resolved", () => {
|
||||||
|
res2(null);
|
||||||
|
resolved = true;
|
||||||
|
});
|
||||||
|
setTimeout(() => !resolved && rej2("timeout"), 10000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pageUtils = {
|
||||||
|
getFile,
|
||||||
|
}
|
42
packages/obsidian-testing-framework/tsconfig.json
Normal file
42
packages/obsidian-testing-framework/tsconfig.json
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"lib": [
|
||||||
|
"es2015",
|
||||||
|
"DOM",
|
||||||
|
"ESNext.Disposable"
|
||||||
|
],
|
||||||
|
|
||||||
|
"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": ["packages/test-project/**/*.ts"]
|
||||||
|
}
|
6
packages/obsidian-testing-framework/typings/global.d.ts
vendored
Normal file
6
packages/obsidian-testing-framework/typings/global.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import {App} from "obsidian";
|
||||||
|
declare global {
|
||||||
|
var app: App;
|
||||||
|
var __callback: <T>(app: App) => T;
|
||||||
|
}
|
||||||
|
export {}
|
10
packages/test-project/.editorconfig
Normal file
10
packages/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
packages/test-project/.eslintignore
Normal file
3
packages/test-project/.eslintignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
|
||||||
|
main.js
|
23
packages/test-project/.eslintrc
Normal file
23
packages/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"
|
||||||
|
}
|
||||||
|
}
|
26
packages/test-project/.gitignore
vendored
Normal file
26
packages/test-project/.gitignore
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# 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
|
||||||
|
/test-results/
|
||||||
|
/playwright-report/
|
||||||
|
/blob-report/
|
||||||
|
/playwright/.cache/
|
1
packages/test-project/.npmrc
Normal file
1
packages/test-project/.npmrc
Normal file
@ -0,0 +1 @@
|
|||||||
|
tag-version-prefix=""
|
96
packages/test-project/README.md
Normal file
96
packages/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
|
22
packages/test-project/e2e/example.spec.ts
Normal file
22
packages/test-project/e2e/example.spec.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { expect } from '@playwright/test';
|
||||||
|
import {test} from "obsidian-testing-framework"
|
||||||
|
import {doWithApp, getApp} from "obsidian-testing-framework/utils";
|
||||||
|
test('something', async ({ page }) => {
|
||||||
|
console.log(page.url());
|
||||||
|
expect(/obsidian\.md/i.test(page.url())).toBeTruthy()
|
||||||
|
});
|
||||||
|
test("idk", async({page}) => {
|
||||||
|
console.log("idk")
|
||||||
|
let what = await doWithApp(page,async (app) => {
|
||||||
|
console.log("hi", Object.keys(app), (app.metadataCache));
|
||||||
|
let thing = app.metadataCache.getFirstLinkpathDest("Welcome", "/");
|
||||||
|
console.log("THING", thing);
|
||||||
|
await new Promise(res => setTimeout(res, 5000))
|
||||||
|
console.log(thing?.path);
|
||||||
|
const what = {...thing};
|
||||||
|
delete what.parent;
|
||||||
|
delete what.vault;
|
||||||
|
return what;
|
||||||
|
});
|
||||||
|
expect(what.basename).toEqual("Welcome")
|
||||||
|
})
|
49
packages/test-project/esbuild.config.mjs
Normal file
49
packages/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
packages/test-project/main.ts
Normal file
134
packages/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
packages/test-project/manifest.json
Normal file
11
packages/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
|
||||||
|
}
|
34
packages/test-project/package.json
Normal file
34
packages/test-project/package.json
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"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",
|
||||||
|
"test": "xvfb-maybe playwright test"
|
||||||
|
},
|
||||||
|
"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",
|
||||||
|
"playwright": "^1.48.1",
|
||||||
|
"tslib": "2.4.0",
|
||||||
|
"typescript": "4.7.4",
|
||||||
|
"vitest": "^2.1.3",
|
||||||
|
"xvfb-maybe": "^0.2.1"
|
||||||
|
},
|
||||||
|
"type": "module"
|
||||||
|
}
|
52
packages/test-project/playwright.config.ts
Normal file
52
packages/test-project/playwright.config.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
import {ObsidianTestFixtures} from "obsidian-testing-framework/fixture";
|
||||||
|
import os from "os"
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<ObsidianTestFixtures>({
|
||||||
|
testDir: './e2e',
|
||||||
|
|
||||||
|
/* 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: 1,
|
||||||
|
/* Opt out of parallel tests on CI. */
|
||||||
|
workers: 1,
|
||||||
|
/* 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',
|
||||||
|
obsidian: {
|
||||||
|
vault: path.resolve(process.cwd(), "..", "..", "vault")
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Configure projects for major browsers */
|
||||||
|
|
||||||
|
|
||||||
|
/* 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
packages/test-project/tests-examples/demo-todo-app.spec.ts
Normal file
437
packages/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);
|
||||||
|
}
|
24
packages/test-project/tsconfig.json
Normal file
24
packages/test-project/tsconfig.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"inlineSourceMap": true,
|
||||||
|
"inlineSources": true,
|
||||||
|
"module": "Node16",
|
||||||
|
"target": "ES6",
|
||||||
|
"allowJs": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"moduleResolution": "Node16",
|
||||||
|
"importHelpers": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"lib": [
|
||||||
|
"DOM",
|
||||||
|
"ES5",
|
||||||
|
"ES6",
|
||||||
|
"ES7"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"**/*.ts"
|
||||||
|
]
|
||||||
|
}
|
14
packages/test-project/version-bump.mjs
Normal file
14
packages/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
packages/test-project/versions.json
Normal file
3
packages/test-project/versions.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"1.0.0": "0.15.0"
|
||||||
|
}
|
1
vault/.obsidian/app.json
vendored
Normal file
1
vault/.obsidian/app.json
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{}
|
1
vault/.obsidian/appearance.json
vendored
Normal file
1
vault/.obsidian/appearance.json
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{}
|
3
vault/.obsidian/community-plugins.json
vendored
Normal file
3
vault/.obsidian/community-plugins.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"obsidian-advanced-uri"
|
||||||
|
]
|
30
vault/.obsidian/core-plugins.json
vendored
Normal file
30
vault/.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/.obsidian/graph.json
vendored
Normal file
22
vault/.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": false
|
||||||
|
}
|
0
vault/.obsidian/plugins/.gitkeep
vendored
Normal file
0
vault/.obsidian/plugins/.gitkeep
vendored
Normal file
159
vault/.obsidian/workspace.json
vendored
Normal file
159
vault/.obsidian/workspace.json
vendored
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
{
|
||||||
|
"main": {
|
||||||
|
"id": "930871bd163b9a7a",
|
||||||
|
"type": "split",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "634dfa4a86e61f7f",
|
||||||
|
"type": "tabs",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "b123fea9f84365d8",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "empty",
|
||||||
|
"state": {},
|
||||||
|
"icon": "lucide-file",
|
||||||
|
"title": "New tab"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"direction": "vertical"
|
||||||
|
},
|
||||||
|
"left": {
|
||||||
|
"id": "a21ebe5947a5c71f",
|
||||||
|
"type": "split",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "32175daed5963aa7",
|
||||||
|
"type": "tabs",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "868b1c060db3a110",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "file-explorer",
|
||||||
|
"state": {
|
||||||
|
"sortOrder": "alphabetical"
|
||||||
|
},
|
||||||
|
"icon": "lucide-folder-closed",
|
||||||
|
"title": "Files"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4bc75e71b51e11b0",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "search",
|
||||||
|
"state": {
|
||||||
|
"query": "",
|
||||||
|
"matchingCase": false,
|
||||||
|
"explainSearch": false,
|
||||||
|
"collapseAll": false,
|
||||||
|
"extraContext": false,
|
||||||
|
"sortOrder": "alphabetical"
|
||||||
|
},
|
||||||
|
"icon": "lucide-search",
|
||||||
|
"title": "Search"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "6d851af11dfb13da",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "bookmarks",
|
||||||
|
"state": {},
|
||||||
|
"icon": "lucide-bookmark",
|
||||||
|
"title": "Bookmarks"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"direction": "horizontal",
|
||||||
|
"width": 300
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"id": "5d6a3e51ae6c758f",
|
||||||
|
"type": "split",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "1c37035be69547e5",
|
||||||
|
"type": "tabs",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "33ec4bd3963497ab",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "backlink",
|
||||||
|
"state": {
|
||||||
|
"collapseAll": false,
|
||||||
|
"extraContext": false,
|
||||||
|
"sortOrder": "alphabetical",
|
||||||
|
"showSearch": false,
|
||||||
|
"searchQuery": "",
|
||||||
|
"backlinkCollapsed": false,
|
||||||
|
"unlinkedCollapsed": true
|
||||||
|
},
|
||||||
|
"icon": "links-coming-in",
|
||||||
|
"title": "Backlinks"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2487192d0b99fdd8",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "outgoing-link",
|
||||||
|
"state": {
|
||||||
|
"linksCollapsed": false,
|
||||||
|
"unlinkedCollapsed": true
|
||||||
|
},
|
||||||
|
"icon": "links-going-out",
|
||||||
|
"title": "Outgoing links"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fec498f9a6d0f797",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "tag",
|
||||||
|
"state": {
|
||||||
|
"sortOrder": "frequency",
|
||||||
|
"useHierarchy": true
|
||||||
|
},
|
||||||
|
"icon": "lucide-tags",
|
||||||
|
"title": "Tags"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5e98b7f33c5bb964",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "outline",
|
||||||
|
"state": {},
|
||||||
|
"icon": "lucide-list",
|
||||||
|
"title": "Outline"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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": "b123fea9f84365d8",
|
||||||
|
"lastOpenFiles": []
|
||||||
|
}
|
5
vault/Welcome.md
Normal file
5
vault/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