test(e2e): add tests to verify timecoded links in Obsidian note's reading mode

This commit is contained in:
Kirill Gavrilov 2025-01-21 21:29:09 +03:00
parent c70eac0027
commit 33fe226baf
10 changed files with 380 additions and 1 deletions

1
e2e/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.e2e_test_vault/

2
e2e/constants.ts Normal file
View file

@ -0,0 +1,2 @@
export const TIMECODES_PLUGIN_ID = 'timecodes-plugin'
export const TEST_VAULT_DIR = '.e2e_test_vault'

21
e2e/package.json Normal file
View file

@ -0,0 +1,21 @@
{
"name": "obsidian-timecodes-plugin-e2e-tests",
"type": "module",
"private": true,
"scripts": {
"pretest": "pnpm --dir .. run build",
"test": "wdio run wdio.conf.ts"
},
"devDependencies": {
"@wdio/cli": "9.5.7",
"@wdio/globals": "9.5.7",
"@wdio/local-runner": "9.5.7",
"@wdio/mocha-framework": "9.5.0",
"@wdio/spec-reporter": "9.5.0",
"electron": "32.2.5",
"expect-webdriverio": "5.0.5",
"obsidian": "1.7.2",
"wdio-electron-service": "7.3.1",
"webdriverio": "9.5.7"
}
}

View file

@ -0,0 +1,183 @@
import ObsidianApp from './pageobjects/obsidian-app.page'
describe('Markdown view post-processor', () => {
context('Link to video and timecode are within the same <p>aragraph in Markdown view', () => {
it('renders timecode with expected clickable link', async () => {
const noteContent = `https://youtu.be/k_ItB5btREU
22:53 - effectiveness characteristics of e2e tests`
await ObsidianApp.createNewNoteWithContent(noteContent)
await ObsidianApp.toggleReadingView()
const link = $('//div[contains(@class, "markdown-preview-section")]//a[text()="22:53"]')
await expect(link).toBePresent()
const href = await link.getAttribute('href')
await expect(href).toBe('https://youtu.be/k_ItB5btREU?t=1373')
})
})
context('Link to video and timecode are in different <p>aragraphs in Markdown view', () => {
it('renders timecode with expected clickable link', async () => {
const noteContent = `https://youtu.be/k_ItB5btREU
22:53 - effectiveness characteristics of e2e tests`
await ObsidianApp.createNewNoteWithContent(noteContent)
await ObsidianApp.toggleReadingView()
const link = $('//div[contains(@class, "markdown-preview-section")]//a[text()="22:53"]')
await expect(link).toBePresent()
const href = await link.getAttribute('href')
await expect(href).toBe('https://youtu.be/k_ItB5btREU?t=1373')
})
})
context('Timecode is on the same line as video link', () => {
it('renders timecode with expected clickable link', async () => {
const noteContent = `https://youtu.be/k_ItB5btREU 22:53 - effectiveness characteristics of e2e tests`
await ObsidianApp.createNewNoteWithContent(noteContent)
await ObsidianApp.toggleReadingView()
const link = $('//div[contains(@class, "markdown-preview-section")]//a[text()="22:53"]')
await expect(link).toBePresent()
const href = await link.getAttribute('href')
await expect(href).toBe('https://youtu.be/k_ItB5btREU?t=1373')
})
})
context('There is a markdown link to YouTube video instead of a plain text link', () => {
it('renders timecode with expected clickable link', async () => {
const noteContent = `[Here](https://youtu.be/k_ItB5btREU) is a great talk about software testing
22:53 - effectiveness characteristics of e2e tests`
await ObsidianApp.createNewNoteWithContent(noteContent)
await ObsidianApp.toggleReadingView()
const link = $('//div[contains(@class, "markdown-preview-section")]//a[text()="22:53"]')
await expect(link).toBePresent()
const href = await link.getAttribute('href')
await expect(href).toBe('https://youtu.be/k_ItB5btREU?t=1373')
})
})
context('The video is embedded with <iframe>', () => {
it('renders timecode with expected clickable link', async () => {
const noteContent = `<iframe width="560" height="315"
src="https://www.youtube.com/embed/k_ItB5btREU?si=RhjVqXP_7K1e-BxG"
title="YouTube video player" frameborder="0" allow="accelerometer;
autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
22:53 - effectiveness characteristics of e2e tests`
await ObsidianApp.createNewNoteWithContent(noteContent)
await ObsidianApp.toggleReadingView()
const link = $('//div[contains(@class, "markdown-preview-section")]//a[text()="22:53"]')
await expect(link).toBePresent()
const href = await link.getAttribute('href')
await expect(href).toBe('https://youtu.be/k_ItB5btREU?t=1373')
})
})
context('There are multiple videos', () => {
it('renders timecodes with expected clickable link', async () => {
const noteContent = `https://www.youtube.com/watch?v=Z5n9VK3sOnI Understanding Gradle classpaths
14:27 a use case for 'api' dependency configuration
https://youtu.be/Lipf5piizZc Another video about Gradle
4:07 - using dependency analysis plugin
https://www.youtube.com/watch?v=k_ItB5btREU
22:53 - effectiveness characteristics of e2e tests`
await ObsidianApp.createNewNoteWithContent(noteContent)
await ObsidianApp.toggleReadingView()
let link = $('//div[contains(@class, "markdown-preview-section")]//a[text()="14:27"]')
await expect(link).toBePresent()
let href = await link.getAttribute('href')
await expect(href).toBe('https://youtu.be/Z5n9VK3sOnI?t=867')
link = $('//div[contains(@class, "markdown-preview-section")]//a[text()="4:07"]')
await expect(link).toBePresent()
href = await link.getAttribute('href')
await expect(href).toBe('https://youtu.be/Lipf5piizZc?t=247')
link = $('//div[contains(@class, "markdown-preview-section")]//a[text()="22:53"]')
await expect(link).toBePresent()
href = await link.getAttribute('href')
await expect(href).toBe('https://youtu.be/k_ItB5btREU?t=1373')
})
})
context('There are multiple videos with mixed links types', () => {
it('renders timecodes with expected clickable link', async () => {
const noteContent = `https://www.youtube.com/watch?v=Z5n9VK3sOnI Understanding Gradle classpaths
14:27 a use case for 'api' dependency configuration
[Another video](https://youtu.be/Lipf5piizZc) about Gradle
4:07 - using dependency analysis plugin
<iframe width="560" height="315"
src="https://www.youtube.com/embed/k_ItB5btREU?si=RhjVqXP_7K1e-BxG"
title="YouTube video player" frameborder="0" allow="accelerometer;
autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
22:53 - effectiveness characteristics of e2e tests`
await ObsidianApp.createNewNoteWithContent(noteContent)
await ObsidianApp.toggleReadingView()
let link = $('//div[contains(@class, "markdown-preview-section")]//a[text()="14:27"]')
await expect(link).toBePresent()
let href = await link.getAttribute('href')
await expect(href).toBe('https://youtu.be/Z5n9VK3sOnI?t=867')
link = $('//div[contains(@class, "markdown-preview-section")]//a[text()="4:07"]')
await expect(link).toBePresent()
href = await link.getAttribute('href')
await expect(href).toBe('https://youtu.be/Lipf5piizZc?t=247')
link = $('//div[contains(@class, "markdown-preview-section")]//a[text()="22:53"]')
await expect(link).toBePresent()
href = await link.getAttribute('href')
await expect(href).toBe('https://youtu.be/k_ItB5btREU?t=1373')
})
})
context('There are multiple videos', () => {
it('preserves the content of a note AS-IS', async () => {
const noteContent = `https://www.youtube.com/watch?v=Z5n9VK3sOnI Understanding Gradle classpaths
14:27 a use case for 'api' dependency configuration; https://youtu.be/Lipf5piizZc 4:07 - using dependency analysis plugin
https://www.youtube.com/watch?v=k_ItB5btREU
22:53 - effectiveness characteristics of e2e tests`
await ObsidianApp.createNewNoteWithContent(noteContent)
await ObsidianApp.toggleReadingView()
/**
* It's important to wait a bit here for note's content to get rendered in reading mode
*/
// eslint-disable-next-line wdio/no-pause
await browser.pause(100)
// eslint-disable-next-line @typescript-eslint/await-thenable, @typescript-eslint/no-base-to-string
const textFromAllNoteParagraphs = await $$('.markdown-preview-section p:not(.mod-ui)')
.map(p => p.getText())
.join('\n\n')
await expect(textFromAllNoteParagraphs).toBe(noteContent)
})
})
})

View file

@ -0,0 +1,84 @@
import * as fs from 'node:fs/promises'
import { App } from 'obsidian'
import { TEST_VAULT_DIR, TIMECODES_PLUGIN_ID } from '../../constants'
class ObsidianApp {
async removeE2eTestVaultIfExists() {
await fs.rm(TEST_VAULT_DIR, { force: true, recursive: true })
}
async createAndOpenFreshVault() {
await browser.execute((testVaultDir: string) => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { ipcRenderer } = require('electron')
const shouldCreateNewVault = true
ipcRenderer.sendSync('vault-open', testVaultDir, shouldCreateNewVault)
}, TEST_VAULT_DIR)
const targetPluginsDir = `${TEST_VAULT_DIR}/.obsidian/plugins/${TIMECODES_PLUGIN_ID}/`
await fs.mkdir(targetPluginsDir, { recursive: true })
await fs.copyFile('../manifest.json', `${targetPluginsDir}/manifest.json`)
await fs.copyFile('../out/main.js', `${targetPluginsDir}/main.js`)
await this.switchToMainWindow()
await $('button=Trust author and enable plugins').click()
await this.closeModal('Trust vault modal')
}
private async switchToMainWindow() {
await browser.switchWindow('app://obsidian.md/index.html')
}
async activateTimecodesPlugin() {
await this.activatePlugin(TIMECODES_PLUGIN_ID)
}
private async activatePlugin(pluginId: string) {
await browser.execute((imgurPluginId: string) => {
// @ts-expect-error 'app' exists in Obsidian
declare const app: App
app.plugins.setEnable(true)
app.plugins.enablePlugin(imgurPluginId)
}, pluginId)
}
async closeModal(modalName: string) {
console.log(`Closing '${modalName}'`)
await $('.modal-close-button').click()
}
async createNewNoteWithContent(content: string) {
await this.doCreateNewNote(content)
}
async createNewNote() {
await this.doCreateNewNote()
}
private async doCreateNewNote(content?: string) {
const newNoteButton = $('aria/New note')
await newNoteButton.click()
const noteContent = $('.workspace-leaf.mod-active .cm-contentContainer')
await noteContent.click()
if (content) {
await browser.execute((content: string) => {
// @ts-expect-error 'app' exists in Obsidian
declare const app: App
app.workspace.activeEditor!.editor!.setValue(content)
}, content)
}
}
async toggleReadingView() {
await browser.execute(() => {
// @ts-expect-error 'app' exists in Obsidian
declare const app: App
app.commands.executeCommandById('markdown:toggle-preview')
})
}
}
export default new ObsidianApp()

29
e2e/tsconfig.json Normal file
View file

@ -0,0 +1,29 @@
{
"compilerOptions": {
"moduleResolution": "node",
"module": "ESNext",
"target": "es2022",
"lib": [
"es2022",
"dom"
],
"types": [
"node",
"@wdio/globals/types",
"expect-webdriverio",
"@wdio/mocha-framework"
],
"skipLibCheck": true,
"noEmit": true,
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": [
"**/*.ts",
],
}

14
e2e/types/obsidian.d.ts vendored Normal file
View file

@ -0,0 +1,14 @@
import Plugin from 'obsidian'
declare module 'obsidian' {
interface App {
plugins: {
plugins: Record<string, Plugin>
setEnable(toggle: boolean): void
enablePlugin(pluginId: string): void
}
commands: {
executeCommandById: (id: string) => boolean
}
}
}

41
e2e/wdio.conf.ts Normal file
View file

@ -0,0 +1,41 @@
/// <reference types="wdio-electron-service" />
import ObsidianApp from './specs/pageobjects/obsidian-app.page'
const debug = process.env.DEBUG
const ONE_DAY = 24 * 60 * 60 * 1000
export const config: WebdriverIO.Config = {
runner: 'local',
specs: ['./specs/*.e2e.ts'],
exclude: [],
maxInstances: 10,
capabilities: [
{
browserName: 'electron',
browserVersion: '32.2.5',
'wdio:electronServiceOptions': {
// custom application args
appBinaryPath: '/Applications/Obsidian.app/Contents/MacOS/Obsidian',
appArgs: [],
},
},
],
logLevel: 'info',
bail: 0,
waitforTimeout: 10000,
connectionRetryTimeout: 120000,
connectionRetryCount: 3,
services: ['electron'],
framework: 'mocha',
reporters: ['spec'],
mochaOpts: {
ui: 'bdd',
timeout: debug ? ONE_DAY : 60000,
},
beforeSuite: async () => {
await ObsidianApp.removeE2eTestVaultIfExists()
await ObsidianApp.createAndOpenFreshVault()
await ObsidianApp.activateTimecodesPlugin()
},
}

View file

@ -89,6 +89,9 @@ export default tseslint.config(
},
},
{
ignores: ['out/*'],
ignores: [
'out/*',
'e2e/.e2e_test_vault/*',
],
},
)

View file

@ -19,4 +19,5 @@
"include": [
"**/*.ts"
],
"exclude": ["e2e/"],
}