mirror of
https://github.com/gavvvr/obsidian-timecodes-plugin.git
synced 2026-07-22 12:00:24 +00:00
test(e2e): add tests to verify timecoded links in Obsidian note's reading mode
This commit is contained in:
parent
c70eac0027
commit
33fe226baf
10 changed files with 380 additions and 1 deletions
1
e2e/.gitignore
vendored
Normal file
1
e2e/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
.e2e_test_vault/
|
||||
2
e2e/constants.ts
Normal file
2
e2e/constants.ts
Normal 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
21
e2e/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
183
e2e/specs/markdown-view_post-processor.e2e.ts
Normal file
183
e2e/specs/markdown-view_post-processor.e2e.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
84
e2e/specs/pageobjects/obsidian-app.page.ts
Normal file
84
e2e/specs/pageobjects/obsidian-app.page.ts
Normal 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
29
e2e/tsconfig.json
Normal 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
14
e2e/types/obsidian.d.ts
vendored
Normal 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
41
e2e/wdio.conf.ts
Normal 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()
|
||||
},
|
||||
}
|
||||
|
|
@ -89,6 +89,9 @@ export default tseslint.config(
|
|||
},
|
||||
},
|
||||
{
|
||||
ignores: ['out/*'],
|
||||
ignores: [
|
||||
'out/*',
|
||||
'e2e/.e2e_test_vault/*',
|
||||
],
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -19,4 +19,5 @@
|
|||
"include": [
|
||||
"**/*.ts"
|
||||
],
|
||||
"exclude": ["e2e/"],
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue