diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 5f0e251..28fa1ed 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -14,98 +14,53 @@ jobs: lint: name: Lint runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: '22' cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Run linter - run: npm run lint + - run: npm ci + - run: npm run lint test: name: Test runs-on: ubuntu-latest - + outputs: + version: ${{ steps.version.outputs.version }} steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: '22' cache: 'npm' + - run: npm ci + - run: npm run test -- --coverage + - id: version + run: echo "version=$(node -p "require('./manifest.json').version")" >> $GITHUB_OUTPUT - - name: Install dependencies - run: npm ci - - - name: Run tests - run: npm run test - - - name: Run tests with coverage - run: npm run test -- --coverage - - build: - name: Build + artifact: + name: Package Artifact runs-on: ubuntu-latest - needs: [lint, test] - + needs: test steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: '22' cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Build plugin - run: npm run build - - - name: Check build artifacts - run: | - if [ ! -f main.js ]; then - echo "Error: main.js not found" - exit 1 - fi - if [ ! -f manifest.json ]; then - echo "Error: manifest.json not found" - exit 1 - fi - if [ ! -f styles.css ]; then - echo "Error: styles.css not found" - exit 1 - fi - echo "✓ All build artifacts present" - - - name: Get version from manifest - id: version - run: echo "version=$(node -p "require('./manifest.json').version")" >> $GITHUB_OUTPUT - + - run: npm ci + - run: npm run build - name: Create plugin package run: | + VERSION=${{ needs.test.outputs.version }} BRANCH_NAME=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}} BRANCH_NAME_SAFE=$(echo "$BRANCH_NAME" | sed 's/[^a-zA-Z0-9._-]/-/g') - ZIP_NAME="git-files-sync-${{ steps.version.outputs.version }}-${BRANCH_NAME_SAFE}.zip" + ZIP_NAME="git-files-sync-${VERSION}-${BRANCH_NAME_SAFE}.zip" zip -j "$ZIP_NAME" main.js manifest.json styles.css echo "ZIP_NAME=$ZIP_NAME" >> $GITHUB_ENV - - - name: Upload build artifacts - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v4 with: - name: plugin-build-${{ steps.version.outputs.version }}-${{ github.sha }} + name: plugin-build-${{ needs.test.outputs.version }}-${{ github.sha }} path: ${{ env.ZIP_NAME }} retention-days: 7 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index 7748ceb..0000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Node.js build - -on: - push: - branches: ["**"] - pull_request: - branches: ["**"] - -jobs: - build: - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [20.x, 22.x] - # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ - - steps: - - uses: actions/checkout@v4 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - cache: "npm" - - run: npm ci - - run: npm run build --if-present - - run: npm run lint - diff --git a/.github/workflows/semantic-release.yml b/.github/workflows/semantic-release.yml index 6dc3197..527873d 100644 --- a/.github/workflows/semantic-release.yml +++ b/.github/workflows/semantic-release.yml @@ -15,71 +15,42 @@ jobs: lint: name: Lint runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: '22' cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Run linter - run: npm run lint + - run: npm ci + - run: npm run lint test: name: Test runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: '22' cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Run tests - run: npm run test - - - name: Run tests with coverage - run: npm run test -- --coverage + - run: npm ci + - run: npm run test -- --coverage release: name: Build and Release runs-on: ubuntu-latest needs: [lint, test] - steps: - - name: Checkout - uses: actions/checkout@v4 + - uses: actions/checkout@v4 with: fetch-depth: 0 persist-credentials: false - - - name: Setup Node.js - uses: actions/setup-node@v4 + - uses: actions/setup-node@v4 with: node-version: '22' cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Build plugin - run: npm run build - - - name: Release - env: + - run: npm ci + - run: npm run build + - env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: npx semantic-release diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml new file mode 100644 index 0000000..d04276a --- /dev/null +++ b/.github/workflows/sonarqube.yml @@ -0,0 +1,47 @@ +name: Build +on: + push: + branches: + - master + - main + pull_request: + types: [opened, synchronize, reopened] + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + - run: npm ci + - run: npm run test -- --coverage + - run: npm run build + + # Upload coverage for the next job + - name: Upload coverage + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: coverage/ + + sonar: + name: SonarQube + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Download coverage + uses: actions/download-artifact@v4 + with: + name: coverage-report + path: coverage/ + - name: SonarQube Scan + uses: SonarSource/sonarqube-scan-action@v7.1.0 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/eslint.config.mts b/eslint.config.mts index 5c66cb8..3062c4a 100644 --- a/eslint.config.mts +++ b/eslint.config.mts @@ -13,25 +13,15 @@ export default tseslint.config( projectService: { allowDefaultProject: [ 'eslint.config.js', - 'manifest.json', - 'vitest.config.ts', + 'manifest.json' ] }, - ttsconfigRootDir: import.meta.dirname, + tsconfigRootDir: import.meta.dirname, extraFileExtensions: ['.json'] }, }, - rules: { - 'no-alert': 'off', // Allow confirm dialogs for user confirmation - '@typescript-eslint/await-thenable': 'off', // Settings methods may not return promises - } }, ...obsidianmd.configs.recommended, - { - rules: { - 'obsidianmd/ui/sentence-case': 'off', - } - }, globalIgnores([ "node_modules", "dist", diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..f51d524 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,14 @@ +sonar.projectKey=firstsun-dev_git-files-sync +sonar.organization=firstsun-dev + + +# This is the name and version displayed in the SonarCloud UI. +#sonar.projectName=git-files-sync +#sonar.projectVersion=1.0 + + +# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. +#sonar.sources=. + +# Encoding of the source code. Default is default system encoding +#sonar.sourceEncoding=UTF-8 diff --git a/src/logic/sync-manager.ts b/src/logic/sync-manager.ts index eb5df37..efdee0b 100644 --- a/src/logic/sync-manager.ts +++ b/src/logic/sync-manager.ts @@ -232,13 +232,11 @@ export class SyncManager { } private async saveSettings() { - /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any */ - // @ts-ignore - access private method to save settings - const plugin = (this.app as any).plugins?.plugins?.['git-file-sync']; - if (plugin) { + const plugins = (this.app as unknown as { plugins: { plugins: Record Promise }> } }).plugins; + const plugin = plugins?.plugins?.['git-file-sync']; + if (plugin && typeof plugin.saveSettings === 'function') { await plugin.saveSettings(); } - /* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any */ } async pushAllFiles(files: (TFile | string)[], onProgress?: (current: number, total: number, fileName: string) => void): Promise<{ success: number; failed: number; errors: Array<{ file: string; error: string }> }> { diff --git a/src/main.ts b/src/main.ts index d296d3e..7cdf646 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,6 +6,7 @@ import { GitServiceInterface } from './services/git-service-interface'; import { SyncManager } from './logic/sync-manager'; import { SyncStatusView, SYNC_STATUS_VIEW_TYPE } from './ui/SyncStatusView'; import { GitignoreManager } from './logic/gitignore-manager'; +import { ConfirmModal } from './ui/ConfirmModal'; export default class GitLabFilesPush extends Plugin { settings: GitLabFilesPushSettings; @@ -40,7 +41,7 @@ export default class GitLabFilesPush extends Plugin { const serviceName = this.settings.serviceType === 'gitlab' ? 'GitLab' : 'GitHub'; - this.addRibbonIcon('upload-cloud', Platform.isMobile ? `Push` : `Push to ${serviceName}`, (evt: MouseEvent) => { + this.addRibbonIcon('upload-cloud', Platform.isMobile ? `Push` : `Push to ${serviceName}`, () => { const activeView = this.app.workspace.getActiveViewOfType(MarkdownView); if (activeView && activeView.file instanceof TFile) { void this.sync.pushFile(activeView.file); @@ -240,9 +241,12 @@ export default class GitLabFilesPush extends Plugin { private showConfirmDialog(message: string): Promise { return new Promise((resolve) => { - // eslint-disable-next-line no-alert - const confirmed = confirm(message); - resolve(confirmed); + new ConfirmModal( + this.app, + message, + () => resolve(true), + () => resolve(false) + ).open(); }); } diff --git a/src/services/github-service.ts b/src/services/github-service.ts index 208cfb2..7842f01 100644 --- a/src/services/github-service.ts +++ b/src/services/github-service.ts @@ -174,7 +174,7 @@ export class GitHubService implements GitServiceInterface { } } - async listFiles(branch: string, path: string = ''): Promise { + async listFiles(branch: string, _path: string = ''): Promise { const url = `https://api.github.com/repos/${this.owner}/${this.repo}/git/trees/${branch}?recursive=1`; const response = await this.safeRequest({ diff --git a/src/settings.ts b/src/settings.ts index 1f35dca..2dea553 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -163,7 +163,7 @@ export class GitLabSyncSettingTab extends PluginSettingTab { private displayGitHubSettings(containerEl: HTMLElement): void { new Setting(containerEl) .setName('GitHub personal access token') - .setDesc('Create a token in GitHub settings > Developer settings > Personal access tokens with "repo" scope') + .setDesc('Create a token in GitHub settings > developer settings > personal access tokens with "repo" scope') .addText(text => text .setPlaceholder('Enter your token') .setValue(this.plugin.settings.githubToken) @@ -177,7 +177,7 @@ export class GitLabSyncSettingTab extends PluginSettingTab { .setName('Repository owner') .setDesc('GitHub username or organization name') .addText(text => text - .setPlaceholder('username') + .setPlaceholder('Username') .setValue(this.plugin.settings.githubOwner) .onChange((value) => { this.plugin.settings.githubOwner = value; @@ -189,7 +189,7 @@ export class GitLabSyncSettingTab extends PluginSettingTab { .setName('Repository name') .setDesc('Name of the GitHub repository') .addText(text => text - .setPlaceholder('my-notes') + .setPlaceholder('My notes') .setValue(this.plugin.settings.githubRepo) .onChange((value) => { this.plugin.settings.githubRepo = value; diff --git a/src/ui/ConfirmModal.ts b/src/ui/ConfirmModal.ts new file mode 100644 index 0000000..08eeaaf --- /dev/null +++ b/src/ui/ConfirmModal.ts @@ -0,0 +1,42 @@ +import { App, Modal, ButtonComponent } from 'obsidian'; + +export class ConfirmModal extends Modal { + private message: string; + private onConfirm: () => void; + private onCancel?: () => void; + + constructor(app: App, message: string, onConfirm: () => void, onCancel?: () => void) { + super(app); + this.message = message; + this.onConfirm = onConfirm; + this.onCancel = onCancel; + } + + onOpen() { + const { contentEl } = this; + contentEl.createEl('h3', { text: 'Confirm' }); + contentEl.createEl('p', { text: this.message }); + + const buttonContainer = contentEl.createDiv({ cls: 'ssv-confirm-buttons modal-button-container' }); + + new ButtonComponent(buttonContainer) + .setButtonText('Cancel') + .onClick(() => { + this.close(); + if (this.onCancel) this.onCancel(); + }); + + new ButtonComponent(buttonContainer) + .setButtonText('Confirm') + .setCta() + .onClick(() => { + this.close(); + this.onConfirm(); + }); + } + + onClose() { + const { contentEl } = this; + contentEl.empty(); + } +} diff --git a/src/ui/SyncStatusView.ts b/src/ui/SyncStatusView.ts index c2c4e6a..31abe23 100644 --- a/src/ui/SyncStatusView.ts +++ b/src/ui/SyncStatusView.ts @@ -1,5 +1,6 @@ import { ItemView, WorkspaceLeaf, TFile, Notice, Platform, setTooltip } from 'obsidian'; import GitLabFilesPush from '../main'; +import { ConfirmModal } from './ConfirmModal'; export const SYNC_STATUS_VIEW_TYPE = 'sync-status-view'; @@ -43,12 +44,13 @@ export class SyncStatusView extends ItemView { getDisplayText(): string { return 'Sync status'; } getIcon(): string { return 'git-compare'; } - async onOpen(): Promise { + onOpen(): Promise { const container = this.containerEl.children[1]; - if (!container) return; + if (!container) return Promise.resolve(); container.empty(); container.addClass('sync-status-view'); this.renderView(); + return Promise.resolve(); } private renderView(): void { @@ -189,7 +191,7 @@ export class SyncStatusView extends ItemView { const delBtn = bar.createEl('button', { cls: 'ssv-btn ssv-btn-delete' }); delBtn.createSpan({ text: '✕' }); - delBtn.createSpan({ cls: 'ssv-btn-label', text: ` Del (${canDelete})` }); + delBtn.createSpan({ cls: 'ssv-btn-label', text: ` Delete (${canDelete})` }); delBtn.disabled = canDelete === 0; setTooltip(delBtn, `Delete ${canDelete} files`); delBtn.addEventListener('click', () => void this.deleteSelected()); @@ -249,7 +251,9 @@ export class SyncStatusView extends ItemView { diffEl.toggleClass('visible', !open); btnLabel.setText(open ? ' Diff' : ' Hide'); const firstChild = diffBtn.firstChild; - if (firstChild) firstChild.textContent = open ? '≡' : '▴'; + if (firstChild instanceof HTMLElement || firstChild instanceof Text) { + firstChild.textContent = open ? '≡' : '▴'; + } }); } @@ -820,12 +824,18 @@ export class SyncStatusView extends ItemView { this.renderView(); } - async onClose(): Promise { /* cleanup */ } + onClose(): Promise { + return Promise.resolve(); + } private showConfirmDialog(message: string): Promise { return new Promise(resolve => { - // eslint-disable-next-line no-alert - resolve(confirm(message)); + new ConfirmModal( + this.app, + message, + () => resolve(true), + () => resolve(false) + ).open(); }); } } diff --git a/tests/logic/sync-manager.test.ts b/tests/logic/sync-manager.test.ts index 62b8301..b5428e3 100644 --- a/tests/logic/sync-manager.test.ts +++ b/tests/logic/sync-manager.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/unbound-method, @typescript-eslint/no-unsafe-return */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { SyncManager } from '../../src/logic/sync-manager'; @@ -98,21 +97,13 @@ describe('SyncManager', () => { const modalMock = vi.mocked(SyncConflictModal); // Capture the callback passed to the modal - let callback: (choice: 'local' | 'remote') => void = () => {}; - modalMock.mockImplementation(function(app, file, local, remote, onChoose) { + let callback: (choice: 'local' | 'remote') => void = () => { }; + modalMock.mockImplementation(function (this: SyncConflictModal, app: App, fileName: string, local: string, remote: string, onChoose: (choice: 'local' | 'remote') => void) { callback = onChoose; - return { - open: vi.fn(), - close: vi.fn(), - app, - scope: {} as unknown, - containerEl: {} as HTMLElement, - contentEl: {} as HTMLElement, - titleEl: {} as HTMLElement, - onOpen: vi.fn(), - onClose: vi.fn(), - setTitle: vi.fn().mockReturnThis(), - } as any; + (this as unknown as Record).open = vi.fn(); + (this as unknown as Record).close = vi.fn(); + (this as unknown as Record).app = app; + (this as unknown as Record).setTitle = vi.fn().mockReturnThis(); }); await manager.pushFile(mockFile); @@ -123,7 +114,7 @@ describe('SyncManager', () => { // Wait for async operations in callback await new Promise(resolve => setTimeout(resolve, 0)); - const pushSpy = mockGitLab.pushFile as any; + const pushSpy = vi.spyOn(mockGitLab, 'pushFile'); expect(pushSpy).toHaveBeenCalledWith('test.md', 'local content', 'main', 'Update test.md from Obsidian', 'remote-sha'); expect(mockSettings.syncMetadata['test.md']?.lastSyncedSha).toBe('new-sha'); }); @@ -138,21 +129,13 @@ describe('SyncManager', () => { const modalMock = vi.mocked(SyncConflictModal); - let callback: (choice: 'local' | 'remote') => void = () => {}; - modalMock.mockImplementation(function(app, file, local, remote, onChoose) { + let callback: (choice: 'local' | 'remote') => void = () => { }; + modalMock.mockImplementation(function (this: SyncConflictModal, app: App, fileName: string, local: string, remote: string, onChoose: (choice: 'local' | 'remote') => void) { callback = onChoose; - return { - open: vi.fn(), - close: vi.fn(), - app, - scope: {} as unknown, - containerEl: {} as HTMLElement, - contentEl: {} as HTMLElement, - titleEl: {} as HTMLElement, - onOpen: vi.fn(), - onClose: vi.fn(), - setTitle: vi.fn().mockReturnThis(), - } as any; + (this as unknown as Record).open = vi.fn(); + (this as unknown as Record).close = vi.fn(); + (this as unknown as Record).app = app; + (this as unknown as Record).setTitle = vi.fn().mockReturnThis(); }); await manager.pushFile(mockFile); @@ -161,7 +144,7 @@ describe('SyncManager', () => { callback('remote'); // Wait for async operations in callback - await new Promise(resolve => setTimeout(resolve, 0)); + await new Promise(resolve => setTimeout(resolve, 50)); expect(modifySpy).toHaveBeenCalledWith(mockFile, 'remote content'); expect(mockSettings.syncMetadata['test.md']?.lastSyncedSha).toBe('remote-sha'); @@ -206,8 +189,10 @@ describe('SyncManager', () => { await manager.pushFile(mockFile); - expect(mockGitLab.getFile).not.toHaveBeenCalled(); - expect(mockGitLab.pushFile).not.toHaveBeenCalled(); + const getFileSpy = vi.spyOn(mockGitLab, 'getFile'); + const pushFileSpy = vi.spyOn(mockGitLab, 'pushFile'); + expect(getFileSpy).not.toHaveBeenCalled(); + expect(pushFileSpy).not.toHaveBeenCalled(); }); it('should add new file to repo when it exists locally but not on remote', async () => { @@ -222,7 +207,8 @@ describe('SyncManager', () => { await manager.pushFile(mockFile); - expect(mockGitLab.pushFile).toHaveBeenCalledWith( + const pushFileSpy = vi.spyOn(mockGitLab, 'pushFile'); + expect(pushFileSpy).toHaveBeenCalledWith( 'new.md', 'new local content', 'main', diff --git a/tests/services/github-service.test.ts b/tests/services/github-service.test.ts new file mode 100644 index 0000000..f256f03 --- /dev/null +++ b/tests/services/github-service.test.ts @@ -0,0 +1,88 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { GitHubService } from '../../src/services/github-service'; +import { requestUrl, RequestUrlResponse, RequestUrlParam } from 'obsidian'; + +describe('GitHubService', () => { + let service: GitHubService; + const token = 'test-token'; + const owner = 'test-owner'; + const repo = 'test-repo'; + + beforeEach(() => { + vi.clearAllMocks(); + service = new GitHubService(token, owner, repo); + }); + + describe('getFile', () => { + it('should fetch and decode file content correctly', async () => { + const mockResponse = { + status: 200, + json: { + content: btoa('hello world'), + sha: 'test-sha' + } + }; + vi.mocked(requestUrl).mockResolvedValue(mockResponse as unknown as RequestUrlResponse); + + const result = await service.getFile('test.md', 'main'); + + expect(result.content).toBe('hello world'); + expect(result.sha).toBe('test-sha'); + }); + + it('should handle 404 correctly and return empty content', async () => { + vi.mocked(requestUrl).mockResolvedValue({ status: 404 } as unknown as RequestUrlResponse); + const result = await service.getFile('missing.md', 'main'); + expect(result.content).toBe(''); + expect(result.sha).toBe(''); + }); + + it('should return sha correctly', async () => { + const mockResponse = { + status: 200, + json: { content: btoa('test'), sha: 'explicit-sha' } + }; + vi.mocked(requestUrl).mockResolvedValue(mockResponse as unknown as RequestUrlResponse); + const result = await service.getFile('test.md', 'main'); + expect(result.sha).toBe('explicit-sha'); + }); + }); + + describe('pushFile', () => { + it('should push new file correctly (no sha provided, remote 404)', async () => { + vi.mocked(requestUrl) + .mockResolvedValueOnce({ status: 404 } as unknown as RequestUrlResponse) // getFile check + .mockResolvedValueOnce({ + status: 201, + json: { content: { path: 'new.md' } } + } as unknown as RequestUrlResponse); // push + + const result = await service.pushFile('new.md', 'new content', 'main', 'create'); + + expect(result).toBe('new.md'); + const calls = vi.mocked(requestUrl).mock.calls; + const lastCallParams = calls[calls.length - 1]; + if (!lastCallParams) throw new Error('lastCall is undefined'); + const lastCall = lastCallParams[0] as RequestUrlParam; + expect(lastCall.method).toBe('PUT'); + expect(lastCall.body).not.toContain('"sha":'); + }); + + it('should update existing file correctly (sha provided)', async () => { + vi.mocked(requestUrl).mockResolvedValue({ + status: 200, + json: { content: { path: 'existing.md' } } + } as unknown as RequestUrlResponse); + + const result = await service.pushFile('existing.md', 'updated content', 'main', 'update', 'old-sha'); + + expect(result).toBe('existing.md'); + const calls = vi.mocked(requestUrl).mock.calls; + const lastCallParams = calls[calls.length - 1]; + if (!lastCallParams) throw new Error('lastCall is undefined'); + const lastCall = lastCallParams[0] as RequestUrlParam; + expect(lastCall.method).toBe('PUT'); + expect(lastCall.body).toContain('"sha":"old-sha"'); + }); + }); +}); diff --git a/tests/services/gitlab-service.test.ts b/tests/services/gitlab-service.test.ts index 0ebd035..ed19999 100644 --- a/tests/services/gitlab-service.test.ts +++ b/tests/services/gitlab-service.test.ts @@ -1,12 +1,7 @@ -/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { GitLabService } from '../../src/services/gitlab-service'; import { requestUrl, RequestUrlResponse } from 'obsidian'; -vi.mock('obsidian', () => ({ - requestUrl: vi.fn(), -})); - describe('GitLabService', () => { let service: GitLabService; const baseUrl = 'https://gitlab.com'; @@ -49,14 +44,14 @@ describe('GitLabService', () => { }); it('should return blob_id as sha', async () => { - const mockResponse: any = { + const mockResponse: unknown = { status: 200, json: { content: btoa('test content'), blob_id: 'test-blob-id' } }; - vi.mocked(requestUrl).mockResolvedValue(mockResponse as unknown as RequestUrlResponse); + vi.mocked(requestUrl).mockResolvedValue(mockResponse as RequestUrlResponse); const result = await service.getFile('test.md', 'main'); expect(result.sha).toBe('test-blob-id'); }); @@ -74,7 +69,7 @@ describe('GitLabService', () => { expect(result).toBe('test.md'); expect(requestUrl).toHaveBeenLastCalledWith(expect.objectContaining({ method: 'POST', - body: expect.stringContaining('bmV3IGNvbnRlbnQ=') + body: expect.stringContaining(btoa('new content')) as unknown as string })); }); @@ -92,7 +87,7 @@ describe('GitLabService', () => { expect(result).toBe('test.md'); expect(requestUrl).toHaveBeenLastCalledWith(expect.objectContaining({ method: 'PUT', - body: expect.stringContaining('dXBkYXRlZCBjb250ZW50') + body: expect.stringContaining(btoa('updated content')) as unknown as string })); }); }); diff --git a/tests/setup.ts b/tests/setup.ts index e77a1c5..da68ab4 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -17,21 +17,21 @@ if (typeof window === 'undefined') { // Mock Obsidian API components export const Plugin = class {}; export const PluginSettingTab = class { - constructor(_app: unknown, _plugin: unknown) {} + constructor() {} }; export const Setting = class { - constructor(_containerEl: unknown) {} - setName(_name: string) { return this; } - setDesc(_desc: string) { return this; } - addText(_cb: unknown) { return this; } - addToggle(_cb: unknown) { return this; } - addButton(_cb: unknown) { return this; } + constructor() {} + setName() { return this; } + setDesc() { return this; } + addText() { return this; } + addToggle() { return this; } + addButton() { return this; } }; export const Notice = class { - constructor(message: string) {} + constructor() {} }; export const Modal = class { - constructor(_app: unknown) {} + constructor() {} open() {} close() {} }; @@ -54,6 +54,7 @@ export const App = class { }; export const TFile = class {}; +export const requestUrl = vi.fn(); vi.mock('obsidian', () => ({ Plugin, @@ -65,4 +66,5 @@ vi.mock('obsidian', () => ({ Editor, App, TFile, + requestUrl, })); diff --git a/tsconfig.json b/tsconfig.json index 3cd62ad..9c2f8a0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,6 +26,7 @@ }, "include": [ "src/**/*.ts", - "tests/**/*.ts" + "tests/**/*.ts", + "vitest.config.ts" ] } diff --git a/version-bump.mjs b/version-bump.mjs index 55d631f..c71cf4d 100644 --- a/version-bump.mjs +++ b/version-bump.mjs @@ -1,4 +1,5 @@ import { readFileSync, writeFileSync } from "fs"; +import process from "process"; const targetVersion = process.env.npm_package_version; diff --git a/vitest.config.ts b/vitest.config.ts index 522204f..ba68b1b 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,6 +1,4 @@ import { defineConfig } from 'vitest/config'; -// eslint-disable-next-line import/no-nodejs-modules -import * as path from 'path'; export default defineConfig({ test: { @@ -8,8 +6,7 @@ export default defineConfig({ globals: true, setupFiles: ['./tests/setup.ts'], alias: { - // eslint-disable-next-line no-undef - 'obsidian': path.resolve(process.cwd(), './tests/setup.ts') + 'obsidian': './tests/setup.ts' } }, });