Improve test-helper (#2820)

This commit is contained in:
Leedehai 2023-12-20 06:03:09 -05:00 committed by GitHub
parent 3dc4eb6bcd
commit 6eb6e877ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 266 additions and 96 deletions

View File

@ -1,87 +1,225 @@
const vscode = require('vscode') const vscode = require('vscode')
const cp = require('child_process') const cp = require('child_process')
function activate(context) { class TestHelper {
let panel = null constructor() {
/** @type {vscode.Uri?} */ this.sourceUriOfActivePanel = null
/** @type {Map<vscode.Uri, vscode.WebviewPanel>} */ this.panels = new Map()
function refreshPanel(stdout, stderr) { /** @type {vscode.StatusBarItem} */ this.testRunningStatusBarItem =
const uri = vscode.window.activeTextEditor.document.uri vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right)
const { pngPath, refPath } = getPaths(uri) this.testRunningStatusBarItem.text = "$(loading~spin) Running"
this.testRunningStatusBarItem.backgroundColor =
new vscode.ThemeColor('statusBarItem.warningBackground')
}
/**
* The caller should ensure when this function is called, a text editor is active.
* Note the fake "editor" for the extension's WebView panel is not one.
* @returns {vscode.Uri}
*/
static getActiveDocumentUri() {
const editor = vscode.window.activeTextEditor
if (!editor) {
throw new Error('vscode.window.activeTextEditor is undefined.')
}
return editor.document.uri
}
/**
* The caller should ensure when this function is called, a WebView panel is active.
* @returns {vscode.Uri}
*/
getSourceUriOfActivePanel() {
// If this function is invoked when user clicks the button from within a WebView
// panel, then the active panel is this panel, and sourceUriOfActivePanel is
// guaranteed to have been updated by that panel's onDidChangeViewState listener.
if (!this.sourceUriOfActivePanel) {
throw new Error('sourceUriOfActivePanel is falsy; is there a focused panel?')
}
return this.sourceUriOfActivePanel
}
/** @param {vscode.Uri} uri */
static getImageUris(uri) {
const png = vscode.Uri.file(uri.path
.replace("tests/typ", "tests/png")
.replace(".typ", ".png"))
const ref = vscode.Uri.file(uri.path
.replace("tests/typ", "tests/ref")
.replace(".typ", ".png"))
return {png, ref}
}
/**
* @param {vscode.Uri} uri
* @param {string} stdout
* @param {string} stderr
*/
refreshTestPreviewImpl_(uri, stdout, stderr) {
const {png, ref} = TestHelper.getImageUris(uri)
const panel = this.panels.get(uri)
if (panel && panel.visible) { if (panel && panel.visible) {
console.log('Refreshing WebView') console.log(`Refreshing WebView for ${uri.fsPath}`)
const pngSrc = panel.webview.asWebviewUri(pngPath) const webViewSrcs = {
const refSrc = panel.webview.asWebviewUri(refPath) png: panel.webview.asWebviewUri(png),
ref: panel.webview.asWebviewUri(ref),
}
panel.webview.html = '' panel.webview.html = ''
// Make refresh notable. // Make refresh notable.
setTimeout(() => { setTimeout(() => {
panel.webview.html = getWebviewContent(pngSrc, refSrc, stdout, stderr) if (!panel) {
throw new Error('panel to refresh is falsy after waiting')
}
panel.webview.html = getWebviewContent(webViewSrcs, stdout, stderr)
}, 50) }, 50)
} }
} }
const openCmd = vscode.commands.registerCommand("ShortcutMenuBar.testOpen", () => { /** @param {vscode.Uri} uri */
panel = vscode.window.createWebviewPanel( openTestPreview(uri) {
'testOutput', if (this.panels.has(uri)) {
'Test output', this.panels.get(uri)?.reveal()
return
}
const newPanel = vscode.window.createWebviewPanel(
'Typst.test-helper.preview',
uri.path.split('/').pop()?.replace('.typ', '.png') ?? 'Test output',
vscode.ViewColumn.Beside, vscode.ViewColumn.Beside,
{}
) )
newPanel.onDidChangeViewState(() => {
refreshPanel("", "") if (newPanel && newPanel.active && newPanel.visible) {
console.log(`Set sourceUriOfActivePanel to ${uri}`)
this.sourceUriOfActivePanel = uri
} else {
console.log(`Set sourceUriOfActivePanel to null`)
this.sourceUriOfActivePanel = null
}
}) })
newPanel.onDidDispose(() => {
const refreshCmd = vscode.commands.registerCommand("ShortcutMenuBar.testRefresh", () => { console.log(`Delete panel ${uri}`)
refreshPanel("", "") this.panels.delete(uri)
if (this.sourceUriOfActivePanel === uri) {
this.sourceUriOfActivePanel = null
}
}) })
this.panels.set(uri, newPanel)
const rerunCmd = vscode.commands.registerCommand("ShortcutMenuBar.testRerun", () => { this.refreshTestPreviewImpl_(uri, "", "")
const uri = vscode.window.activeTextEditor.document.uri }
/** @param {vscode.Uri} uri */
runTest(uri) {
const components = uri.fsPath.split(/tests[\/\\]/) const components = uri.fsPath.split(/tests[\/\\]/)
const dir = components[0] const dir = components[0]
const subPath = components[1] const subPath = components[1]
this.testRunningStatusBarItem.show()
cp.exec( cp.exec(
`cargo test --manifest-path ${dir}/Cargo.toml --all --test tests -- ${subPath}`, `cargo test --manifest-path ${dir}/Cargo.toml --all --test tests -- ${subPath}`,
(err, stdout, stderr) => { (err, stdout, stderr) => {
console.log('Ran tests') this.testRunningStatusBarItem.hide()
refreshPanel(stdout, stderr) console.log(`Ran tests ${uri.fsPath}`)
this.refreshTestPreviewImpl_(uri, stdout, stderr)
} }
) )
}) }
const updateCmd = vscode.commands.registerCommand("ShortcutMenuBar.testUpdate", () => { /** @param {vscode.Uri} uri */
const uri = vscode.window.activeTextEditor.document.uri refreshTestPreview(uri) {
const { pngPath, refPath } = getPaths(uri) const panel = this.panels.get(uri)
if (panel) {
panel.reveal()
this.refreshTestPreviewImpl_(uri, "", "")
}
}
vscode.workspace.fs.copy(pngPath, refPath, { overwrite: true }).then(() => { /** @param {vscode.Uri} uri */
console.log('Copied to reference file') updateTestReference(uri) {
cp.exec(`oxipng -o max -a ${refPath.fsPath}`, (err, stdout, stderr) => { const {png, ref} = TestHelper.getImageUris(uri)
refreshPanel(stdout, stderr)
})
})
})
context.subscriptions.push(openCmd) vscode.workspace.fs.copy(png, ref, {overwrite: true})
context.subscriptions.push(refreshCmd) .then(() => {
context.subscriptions.push(rerunCmd) cp.exec(`oxipng -o max -a ${ref.fsPath}`, (err, stdout, stderr) => {
context.subscriptions.push(updateCmd) console.log(`Copied to reference file for ${uri.fsPath}`)
this.refreshTestPreviewImpl_(uri, stdout, stderr)
})
})
}
/**
* @param {vscode.Uri} uri
* @param {string} webviewSection
*/
copyFilePathToClipboard(uri, webviewSection) {
const {png, ref} = TestHelper.getImageUris(uri)
switch (webviewSection) {
case 'png':
vscode.env.clipboard.writeText(png.fsPath)
break
case 'ref':
vscode.env.clipboard.writeText(ref.fsPath)
break
default:
break
}
}
} }
function getPaths(uri) { /** @param {vscode.ExtensionContext} context */
const pngPath = vscode.Uri.file(uri.path function activate(context) {
.replace("tests/typ", "tests/png") const manager = new TestHelper();
.replace(".typ", ".png")) context.subscriptions.push(manager.testRunningStatusBarItem)
const refPath = vscode.Uri.file(uri.path context.subscriptions.push(vscode.commands.registerCommand(
.replace("tests/typ", "tests/ref") "Typst.test-helper.openFromSource", () => {
.replace(".typ", ".png")) manager.openTestPreview(TestHelper.getActiveDocumentUri())
}))
return { pngPath, refPath } context.subscriptions.push(vscode.commands.registerCommand(
"Typst.test-helper.refreshFromSource", () => {
manager.refreshTestPreview(TestHelper.getActiveDocumentUri())
}))
context.subscriptions.push(vscode.commands.registerCommand(
"Typst.test-helper.refreshFromPreview", () => {
manager.refreshTestPreview(manager.getSourceUriOfActivePanel())
}))
context.subscriptions.push(vscode.commands.registerCommand(
"Typst.test-helper.runFromSource", () => {
manager.runTest(TestHelper.getActiveDocumentUri())
}))
context.subscriptions.push(vscode.commands.registerCommand(
"Typst.test-helper.runFromPreview", () => {
manager.runTest(manager.getSourceUriOfActivePanel())
}))
context.subscriptions.push(vscode.commands.registerCommand(
"Typst.test-helper.updateFromSource", () => {
manager.updateTestReference(TestHelper.getActiveDocumentUri())
}))
context.subscriptions.push(vscode.commands.registerCommand(
"Typst.test-helper.updateFromPreview", () => {
manager.updateTestReference(manager.getSourceUriOfActivePanel())
}))
// Context menu: the drop-down menu after right-click.
context.subscriptions.push(vscode.commands.registerCommand(
"Typst.test-helper.copyImageFilePathFromPreviewContext", (e) => {
manager.copyFilePathToClipboard(
manager.getSourceUriOfActivePanel(), e.webviewSection)
}))
} }
function getWebviewContent(pngSrc, refSrc, stdout, stderr) { /**
* @param {{png: vscode.Uri, ref: vscode.Uri}} webViewSrcs
* @param {string} stdout
* @param {string} stderr
* @returns {string}
*/
function getWebviewContent(webViewSrcs, stdout, stderr) {
const escape = (text) => text.replace(/</g, "&lt;").replace(/>/g, "&gt;")
return ` return `
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
@ -118,15 +256,15 @@ function getWebviewContent(pngSrc, refSrc, stdout, stderr) {
</style> </style>
</head> </head>
<body> <body>
<div class="flex"> <div class="flex" data-vscode-context='{"preventDefaultContextMenuItems": true}'>
<div> <div>
<h1>Output</h1> <h1>Output</h1>
<img src="${pngSrc}"/> <img data-vscode-context='{"webviewSection":"png"}' src="${webViewSrcs.png}"/>
</div> </div>
<div> <div>
<h1>Reference</h1> <h1>Reference</h1>
<img src="${refSrc}"/> <img data-vscode-context='{"webviewSection":"ref"}' src="${webViewSrcs.ref}"/>
</div> </div>
</div> </div>
@ -140,10 +278,6 @@ function getWebviewContent(pngSrc, refSrc, stdout, stderr) {
` `
} }
function escape(text) {
return text.replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
function deactivate() {} function deactivate() {}
module.exports = { activate, deactivate } module.exports = {activate, deactivate}

View File

@ -1,81 +1,117 @@
{ {
"name": "typst-test-helper", "name": "test-helper",
"publisher": "typst",
"displayName": "Typst Test Helper", "displayName": "Typst Test Helper",
"description": "Helps to run, compare and update Typst tests.", "description": "Helps to run, compare and update Typst tests.",
"version": "0.0.1", "version": "0.0.1",
"engines": { "engines": {
"vscode": "^1.53.0" "vscode": "^1.71.0"
}, },
"categories": [ "categories": [
"Other" "Other"
], ],
"activationEvents": [ "activationEvents": [
"onCommand:ShortcutMenuBar.testOpen", "onCommand:Typst.test-helper.openFromSource",
"onCommand:ShortcutMenuBar.testRefresh", "onCommand:Typst.test-helper.refreshFromSource",
"onCommand:ShortcutMenuBar.testRerun", "onCommand:Typst.test-helper.refreshFromPreview",
"onCommand:ShortcutMenuBar.testUpdate" "onCommand:Typst.test-helper.runFromSource",
"onCommand:Typst.test-helper.runFromPreview",
"onCommand:Typst.test-helper.updateFromSource",
"onCommand:Typst.test-helper.updateFromPreview",
"onCommand:Typst.test-helper.copyImageFilePathFromPreviewContext"
], ],
"main": "./extension.js", "main": "./extension.js",
"contributes": { "contributes": {
"commands": [ "commands": [
{ {
"command": "ShortcutMenuBar.testOpen", "command": "Typst.test-helper.openFromSource",
"title": "Open test output", "title": "Open test output",
"category": "ShortcutMenuBar", "category": "Typst.test-helper",
"icon": { "icon": "$(plus)"
"light": "images/open-light.svg",
"dark": "images/open-dark.svg"
}
}, },
{ {
"command": "ShortcutMenuBar.testRefresh", "command": "Typst.test-helper.refreshFromSource",
"title": "Refresh preview", "title": "Refresh preview",
"category": "ShortcutMenuBar", "category": "Typst.test-helper",
"icon": { "icon": "$(refresh)"
"light": "images/refresh-light.svg",
"dark": "images/refresh-dark.svg"
}
}, },
{ {
"command": "ShortcutMenuBar.testRerun", "command": "Typst.test-helper.refreshFromPreview",
"title": "Rerun test", "title": "Refresh preview",
"category": "ShortcutMenuBar", "category": "Typst.test-helper",
"icon": { "icon": "$(refresh)"
"light": "images/rerun-light.svg",
"dark": "images/rerun-dark.svg"
}
}, },
{ {
"command": "ShortcutMenuBar.testUpdate", "command": "Typst.test-helper.runFromSource",
"title": "Run test",
"category": "Typst.test-helper",
"icon": "$(debug-start)"
},
{
"command": "Typst.test-helper.runFromPreview",
"title": "Run test",
"category": "Typst.test-helper",
"icon": "$(debug-start)"
},
{
"command": "Typst.test-helper.updateFromSource",
"title": "Update reference image", "title": "Update reference image",
"category": "ShortcutMenuBar", "category": "Typst.test-helper",
"icon": { "icon": "$(save)"
"light": "images/update-light.svg", },
"dark": "images/update-dark.svg" {
} "command": "Typst.test-helper.updateFromPreview",
"title": "Update reference image",
"category": "Typst.test-helper",
"icon": "$(save)"
},
{
"command": "Typst.test-helper.copyImageFilePathFromPreviewContext",
"title": "Copy image file path"
} }
], ],
"menus": { "menus": {
"editor/title": [ "editor/title": [
{ {
"when": "resourceExtname == .typ && resourcePath =~ /.*tests.*/", "when": "resourceExtname == .typ && resourcePath =~ /.*tests.*/",
"command": "ShortcutMenuBar.testOpen", "command": "Typst.test-helper.openFromSource",
"group": "navigation@0"
},
{
"when": "resourceExtname == .typ && resourcePath =~ /.*tests.*/",
"command": "ShortcutMenuBar.testRefresh",
"group": "navigation@1" "group": "navigation@1"
}, },
{ {
"when": "resourceExtname == .typ && resourcePath =~ /.*tests.*/", "when": "resourceExtname == .typ && resourcePath =~ /.*tests.*/",
"command": "ShortcutMenuBar.testRerun", "command": "Typst.test-helper.refreshFromSource",
"group": "navigation@2" "group": "navigation@2"
}, },
{ {
"when": "resourceExtname == .typ && resourcePath =~ /.*tests.*/", "when": "resourceExtname == .typ && resourcePath =~ /.*tests.*/",
"command": "ShortcutMenuBar.testUpdate", "command": "Typst.test-helper.runFromSource",
"group": "navigation@3" "group": "navigation@3"
},
{
"when": "resourceExtname == .typ && resourcePath =~ /.*tests.*/",
"command": "Typst.test-helper.updateFromSource",
"group": "navigation@4"
},
{
"when": "activeWebviewPanelId == Typst.test-helper.preview",
"command": "Typst.test-helper.refreshFromPreview",
"group": "navigation@1"
},
{
"when": "activeWebviewPanelId == Typst.test-helper.preview",
"command": "Typst.test-helper.runFromPreview",
"group": "navigation@2"
},
{
"when": "activeWebviewPanelId == Typst.test-helper.preview",
"command": "Typst.test-helper.updateFromPreview",
"group": "navigation@3"
}
],
"webview/context": [
{
"command": "Typst.test-helper.copyImageFilePathFromPreviewContext",
"when": "webviewId == Typst.test-helper.preview && (webviewSection == png || webviewSection == ref)"
} }
] ]
} }