Skip to content

Commit

Permalink
First batch of pdf viewer unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
James-Yu committed Sep 23, 2024
1 parent 9ff6c9c commit 4abc6e6
Show file tree
Hide file tree
Showing 6 changed files with 261 additions and 30 deletions.
14 changes: 7 additions & 7 deletions src/preview/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ function getPort(): number {

async function getUrl(pdfUri?: vscode.Uri): Promise<{url: string, uri: vscode.Uri}> {
// viewer/viewer.js automatically requests the file to server.ts, and server.ts decodes the encoded path of PDF file.
const origUrl = await vscode.env.asExternalUri(vscode.Uri.parse(`http://127.0.0.1:${lw.server.getPort()}`, true))
const origUrl = await vscode.env.asExternalUri(vscode.Uri.parse(`http://127.0.0.1:${getPort()}`, true))
const url =
(origUrl.toString().endsWith('/') ? origUrl.toString().slice(0, -1) : origUrl.toString()) +
(pdfUri ? ('/viewer.html?file=' + encodePathWithPrefix(pdfUri)) : '')
Expand Down Expand Up @@ -166,7 +166,7 @@ function checkHttpOrigin(req: http.IncomingMessage, response: http.ServerRespons
}
}

function sendOkResponse(response: http.ServerResponse, content: string, contentType: string, cors: boolean = true) {
function sendOkResponse(response: http.ServerResponse, content: Buffer, contentType: string, cors: boolean = true) {
//
// Headers to enable site isolation.
// - https://fetch.spec.whatwg.org/#cross-origin-resource-policy-header
Expand Down Expand Up @@ -204,8 +204,8 @@ async function handler(request: http.IncomingMessage, response: http.ServerRespo
return
}
try {
const content = await lw.file.read(fileUri, true)
sendOkResponse(response, content ?? '', 'application/pdf')
const content = await vscode.workspace.fs.readFile(fileUri)
sendOkResponse(response, Buffer.from(content), 'application/pdf')
logger.log(`Preview PDF file: ${fileUri.toString(true)}`)
} catch (e) {
logger.logError(`Error reading PDF ${fileUri.toString(true)}`, e)
Expand All @@ -216,7 +216,7 @@ async function handler(request: http.IncomingMessage, response: http.ServerRespo
}
if (request.url.endsWith('/config.json')) {
const params = lw.viewer.getParams()
sendOkResponse(response, JSON.stringify(params), 'application/json')
sendOkResponse(response, Buffer.from(JSON.stringify(params)), 'application/json')
return
}
let root: string
Expand Down Expand Up @@ -287,8 +287,8 @@ async function handler(request: http.IncomingMessage, response: http.ServerRespo
}
}
try {
const content = await lw.file.read(fileName, true)
sendOkResponse(response, content ?? '', contentType, false)
const content = await vscode.workspace.fs.readFile(vscode.Uri.file(fileName))
sendOkResponse(response, Buffer.from(content), contentType, false)
} catch (err) {
if (typeof (err as any).code === 'string' && (err as any).code === 'FileNotFound') {
response.writeHead(404)
Expand Down
7 changes: 2 additions & 5 deletions src/preview/viewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,7 @@ async function viewInCustomEditor(pdfFile: string): Promise<void> {
await vscode.commands.executeCommand('workbench.action.focusRightGroup')
} else {
await vscode.commands.executeCommand('vscode.openWith', pdfUri, 'latex-workshop-pdf-hook', showOptions)
if (currentColumn === vscode.ViewColumn.One) {
await moveActiveEditor('left', true)
} else {
await vscode.commands.executeCommand('workbench.action.focusRightGroup')
}
await moveActiveEditor('left', true)
}
} else if (editorGroup === 'right') {
const currentColumn = vscode.window.activeTextEditor?.viewColumn
Expand Down Expand Up @@ -444,6 +440,7 @@ function showInvisibleWebviewPanel(pdfUri: vscode.Uri): boolean {
}

/**
* !! Test only
* Returns the state of the internal PDF viewer of `pdfFilePath`.
*
* @param pdfUri The path of a PDF file.
Expand Down
Binary file added test/fixtures/unittest/10_viewer_pdf_server/main.pdf
Binary file not shown.
34 changes: 18 additions & 16 deletions test/units/09_viewer_server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ describe(path.basename(__filename).split('.')[0] + ':', () => {
await connectWs()
})

after(() => {
sinon.restore()
})

async function connectWs() {
const serverPath = `ws://127.0.0.1:${lw.server.getPort()}`
websocket = new ws.WebSocket(serverPath)
Expand All @@ -27,26 +31,24 @@ describe(path.basename(__filename).split('.')[0] + ':', () => {
})
}

async function waitMessage(msg: ClientRequest, timeout = 1000) {
const msgString = JSON.stringify(msg)
let elapsed = 0
while(true) {
if (handlerStub.called && (handlerStub.lastCall.args?.[1] as Uint8Array).toString() === msgString) {
break
}
await sleep(10)
elapsed += 10
if (elapsed >= timeout) {
assert.fail(`Timed out waiting for message ${msgString}`)
}
}
}

describe('lw.viewer->server.WsServer', () => {
it('should handle websocket messages', async () => {
handlerStub.resetHistory()
websocket.send(JSON.stringify({ type: 'ping' }))
await waitMessage({ type: 'ping' })
let elapsed = 0
while (true) {
if (
handlerStub.called &&
(JSON.parse((handlerStub.lastCall.args?.[1] as Uint8Array).toString()) as ClientRequest).type === 'ping'
) {
break
}
await sleep(10)
elapsed += 10
if (elapsed >= 1000) {
assert.fail('Timed out waiting for message "ping"')
}
}
})
})

Expand Down
229 changes: 229 additions & 0 deletions test/units/10_viewer_pdf_server.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import * as vscode from 'vscode'
import * as path from 'path'
import * as sinon from 'sinon'
import { lw } from '../../src/lw'
import { view, viewInWebviewPanel } from '../../src/preview/viewer'
import * as manager from '../../src/preview/viewer/pdfviewermanager'
import { assert, get, mock, set, sleep } from './utils'
import type { ClientRequest } from '../../types/latex-workshop-protocol-types'

describe.only(path.basename(__filename).split('.')[0] + ':', () => {
const fixture = path.basename(__filename).split('.')[0]
const pdfPath = get.path(fixture, 'main.pdf')
const pdfUri = vscode.Uri.file(pdfPath)
let handlerSpy: sinon.SinonSpy

before(() => {
mock.init(lw, 'file', 'root', 'server', 'viewer')
handlerSpy = sinon.spy(lw.viewer, 'handler')
})

afterEach(async () => {
await vscode.commands.executeCommand('workbench.action.closeAllEditors')
})

after(() => {
sinon.restore()
})

function waitMessage(type: ClientRequest['type'], timeout = 1000) {
return (async () => {
handlerSpy.resetHistory()
let elapsed = 0
while (true) {
if (
handlerSpy.called &&
(JSON.parse((handlerSpy.lastCall.args?.[1] as Uint8Array).toString()) as ClientRequest).type === type
) {
break
}
await sleep(10)
elapsed += 10
if (elapsed >= timeout) {
assert.fail(`Timed out waiting for message "${type}"`)
}
}
})()
}

describe('lw.viewer->viewer.viewInCustomEditor', () => {
let execSpy: sinon.SinonSpy

before(() => {
execSpy = sinon.spy(vscode.commands, 'executeCommand')
})

beforeEach(() => {
set.config('viewer.pdf.viewer', 'tab')
execSpy.resetHistory()
})

after(() => {
execSpy.restore()
})

it('should create a custom editor', async () => {
const promise = waitMessage('loaded')
await view(pdfPath)
await promise

assert.hasLog(`Open PDF tab for ${pdfUri.toString(true)}`)
})

it('should register the created panel in the viewer manager', async () => {
const promise = waitMessage('loaded')
await view(pdfPath)
await promise

assert.strictEqual(manager.getPanels(pdfUri)?.size, 1)
})

it('should create the custom editor at the left group if focused on right', async () => {
set.config('view.pdf.tab.editorGroup', 'left')
mock.activeTextEditor('main.tex', '', { viewColumn: vscode.ViewColumn.Two })

await view(pdfPath)

assert.strictEqual(execSpy.callCount, 2)
assert.strictEqual(execSpy.firstCall.args[0], 'vscode.openWith')
assert.strictEqual((execSpy.firstCall.args[3] as vscode.TextDocumentShowOptions).viewColumn, 1)
assert.strictEqual(execSpy.secondCall.args[0], 'workbench.action.focusRightGroup')
})

it('should create the custom editor and move to the left group if focused on left', async () => {
set.config('view.pdf.tab.editorGroup', 'left')
mock.activeTextEditor('main.tex', '', { viewColumn: vscode.ViewColumn.One })

await view(pdfPath)

assert.strictEqual(execSpy.callCount, 3)
assert.strictEqual(execSpy.firstCall.args[0], 'vscode.openWith')
assert.strictEqual((execSpy.firstCall.args[3] as vscode.TextDocumentShowOptions).viewColumn, -1)
assert.strictEqual(execSpy.secondCall.args[0], 'workbench.action.moveEditorToLeftGroup')
assert.strictEqual(execSpy.thirdCall.args[0], 'workbench.action.focusRightGroup')
})

it('should create the custom editor to the right', async () => {
set.config('view.pdf.tab.editorGroup', 'right')
mock.activeTextEditor('main.tex', '', { viewColumn: vscode.ViewColumn.One })

await view(pdfPath)

assert.strictEqual(execSpy.callCount, 2)
assert.strictEqual(execSpy.firstCall.args[0], 'vscode.openWith')
assert.strictEqual((execSpy.firstCall.args[3] as vscode.TextDocumentShowOptions).viewColumn, 2)
assert.strictEqual(execSpy.secondCall.args[0], 'workbench.action.focusLeftGroup')
})

it('should create the custom editor and move to above or below', async () => {
set.config('view.pdf.tab.editorGroup', 'above')
mock.activeTextEditor('main.tex', '', { viewColumn: vscode.ViewColumn.One })

await view(pdfPath)

assert.strictEqual(execSpy.callCount, 3)
assert.strictEqual(execSpy.firstCall.args[0], 'vscode.openWith')
assert.strictEqual(execSpy.secondCall.args[0], 'workbench.action.moveEditorToAboveGroup')
assert.strictEqual(execSpy.thirdCall.args[0], 'workbench.action.focusBelowGroup')

execSpy.resetHistory()
set.config('view.pdf.tab.editorGroup', 'below')
await view(pdfPath)
assert.strictEqual(execSpy.callCount, 3)
assert.strictEqual(execSpy.firstCall.args[0], 'vscode.openWith')
assert.strictEqual(execSpy.secondCall.args[0], 'workbench.action.moveEditorToBelowGroup')
assert.strictEqual(execSpy.thirdCall.args[0], 'workbench.action.focusAboveGroup')
})
})

describe('lw.viewer->viewer.viewInWebviewPanel', () => {
let execSpy: sinon.SinonSpy

before(() => {
execSpy = sinon.spy(vscode.commands, 'executeCommand')
})

beforeEach(() => {
execSpy.resetHistory()
})

after(() => {
execSpy.restore()
})

it('should create a webview panel', async () => {
const promise = waitMessage('loaded')
await viewInWebviewPanel(pdfUri, 'current', true)
await promise

assert.hasLog(`Open PDF tab for ${pdfUri.toString(true)}`)
})

it('should register the created panel in the viewer manager', async () => {
const promise = waitMessage('loaded')
await viewInWebviewPanel(pdfUri, 'current', true)
await promise

assert.strictEqual(manager.getPanels(pdfUri)?.size, 1)
})

it('should move the webview panel to the specified editor group', async () => {
const activeEditorStub = mock.activeTextEditor('main.tex', '')

execSpy.resetHistory()
await viewInWebviewPanel(pdfUri, 'left', true)
assert.strictEqual(execSpy.callCount, 2)
assert.strictEqual(execSpy.firstCall.args[0], 'workbench.action.moveEditorToLeftGroup')
assert.strictEqual(execSpy.secondCall.args[0], 'workbench.action.focusRightGroup')

execSpy.resetHistory()
await viewInWebviewPanel(pdfUri, 'right', true)
assert.strictEqual(execSpy.callCount, 2)
assert.strictEqual(execSpy.firstCall.args[0], 'workbench.action.moveEditorToRightGroup')
assert.strictEqual(execSpy.secondCall.args[0], 'workbench.action.focusLeftGroup')

execSpy.resetHistory()
await viewInWebviewPanel(pdfUri, 'above', true)
assert.strictEqual(execSpy.callCount, 2)
assert.strictEqual(execSpy.firstCall.args[0], 'workbench.action.moveEditorToAboveGroup')
assert.strictEqual(execSpy.secondCall.args[0], 'workbench.action.focusBelowGroup')

execSpy.resetHistory()
await viewInWebviewPanel(pdfUri, 'below', true)
assert.strictEqual(execSpy.callCount, 2)
assert.strictEqual(execSpy.firstCall.args[0], 'workbench.action.moveEditorToBelowGroup')
assert.strictEqual(execSpy.secondCall.args[0], 'workbench.action.focusAboveGroup')

activeEditorStub.restore()
})

it('should not move the webview panel if there is no active editor', async () => {
const activeEditorStub = sinon.stub(vscode.window, 'activeTextEditor').value(undefined)
await viewInWebviewPanel(pdfUri, 'left', true)
activeEditorStub.restore()

assert.strictEqual(execSpy.callCount, 0)
})

it('should only move the webview panel but not focus back if `preserveFocus` is `false`', async () => {
const activeEditorStub = mock.activeTextEditor('main.tex', '')

await viewInWebviewPanel(pdfUri, 'left', false)

activeEditorStub.restore()
assert.strictEqual(execSpy.callCount, 1)
assert.strictEqual(execSpy.firstCall.args[0], 'workbench.action.moveEditorToLeftGroup')
})
})

describe('lw.viewer->viewer.viewInTab', () => {
it('should create a webview panel', async () => {
set.config('viewer.pdf.viewer', 'legacy')
const promise = waitMessage('loaded')
await view(pdfPath, 'tab')
await promise

assert.hasLog(`Open PDF tab for ${pdfUri.toString(true)}`)
})
})
})
7 changes: 5 additions & 2 deletions test/units/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ export const mock = {
textDocument: (filePath: string, content: string, params: { languageId?: string, isDirty?: boolean, isClosed?: boolean, scheme?: string } = {}) => {
return sinon.stub(vscode.workspace, 'textDocuments').value([ new TextDocument(filePath, content, params) ])
},
activeTextEditor: (filePath: string, content: string, params: { languageId?: string, isDirty?: boolean, isClosed?: boolean, scheme?: string } = {}) => {
activeTextEditor: (filePath: string, content: string, params: { languageId?: string, isDirty?: boolean, isClosed?: boolean, scheme?: string, viewColumn?: vscode.ViewColumn } = {}) => {
return sinon.stub(vscode.window, 'activeTextEditor').value(new TextEditor(filePath, content, params))
}
}
Expand Down Expand Up @@ -255,8 +255,11 @@ class TextEditor implements vscode.TextEditor {
options: vscode.TextEditorOptions = {}
viewColumn: vscode.ViewColumn | undefined = vscode.ViewColumn.Active

constructor(filePath: string, content: string, { languageId = 'latex', isDirty = false, isClosed = false, scheme = 'file' }: { languageId?: string, isDirty?: boolean, isClosed?: boolean, scheme?: string }) {
constructor(filePath: string, content: string, { languageId = 'latex', isDirty = false, isClosed = false, scheme = 'file', viewColumn = undefined }: { languageId?: string, isDirty?: boolean, isClosed?: boolean, scheme?: string, viewColumn?: vscode.ViewColumn }) {
this.document = new TextDocument(filePath, content, { languageId, isDirty, isClosed, scheme })
if (viewColumn !== undefined) {
this.viewColumn = viewColumn
}
}

edit(_: (_: vscode.TextEditorEdit) => void): Thenable<boolean> { throw new Error('Not implemented.') }
Expand Down

0 comments on commit 4abc6e6

Please sign in to comment.