From 71ce71ee11d6a9f7c135253f15e96f47109908bc Mon Sep 17 00:00:00 2001 From: Moritz Date: Thu, 26 Jan 2023 17:08:47 +0100 Subject: [PATCH] Add golden test (#8) * Add golden test * Small changes as per review --- src/extension.ts | 1 - src/parseAndDecorate.ts | 28 +++++----- src/test/suite/extension.test.ts | 74 ++++++++++++++++++++++--- src/test/testarb.annotated | 92 ++++++++++++++++++++++++++++++++ {test => src/test}/testarb.arb | 0 5 files changed, 174 insertions(+), 21 deletions(-) create mode 100644 src/test/testarb.annotated rename {test => src/test}/testarb.arb (100%) diff --git a/src/extension.ts b/src/extension.ts index 3660e9b..275168b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -67,7 +67,6 @@ export async function activate(context: vscode.ExtensionContext) { decoratorAndParser.parseAndDecorate(activeTextEditor); } - // At extension startup } diff --git a/src/parseAndDecorate.ts b/src/parseAndDecorate.ts index 84a5690..e8e7aa4 100644 --- a/src/parseAndDecorate.ts +++ b/src/parseAndDecorate.ts @@ -14,7 +14,7 @@ import { JSONPath, visit } from 'jsonc-parser'; import * as vscode from 'vscode'; import XRegExp = require('xregexp'); -const argDecoration = vscode.window.createTextEditorDecorationType({ +export const argDecoration = vscode.window.createTextEditorDecorationType({ light: { color: '#ff6f00' }, @@ -22,7 +22,7 @@ const argDecoration = vscode.window.createTextEditorDecorationType({ color: '#fff9c4' } }); -const selectDecoration = vscode.window.createTextEditorDecorationType({ +export const selectDecoration = vscode.window.createTextEditorDecorationType({ light: { color: '#6a1b9a' }, @@ -30,7 +30,7 @@ const selectDecoration = vscode.window.createTextEditorDecorationType({ color: '#ce93d8' } }); -const pluralDecoration = vscode.window.createTextEditorDecorationType({ +export const pluralDecoration = vscode.window.createTextEditorDecorationType({ light: { color: '#0277bd' }, @@ -58,11 +58,11 @@ class Literal { export class DecoratorAndParser { diagnostics = vscode.languages.createDiagnosticCollection("arb"); - constructor(context: vscode.ExtensionContext) { - context.subscriptions.push(this.diagnostics); + constructor(context?: vscode.ExtensionContext) { + context?.subscriptions.push(this.diagnostics); } - parseAndDecorate(editor: vscode.TextEditor) { + parseAndDecorate(editor: vscode.TextEditor): { diagnostics: vscode.Diagnostic[]; decorations: Map; } | null { // Prefill decorations map to avoid having old decoration hanging around let decorationsMap = new Map([ [argDecoration, []], @@ -76,7 +76,7 @@ export class DecoratorAndParser { // Only trigger on arb files if (!editor || !path.basename(editor.document.fileName).endsWith('.arb')) { - return; + return null; } let nestingLevel = 0; let placeholderLevel: number | null; @@ -103,7 +103,7 @@ export class DecoratorAndParser { onObjectProperty: (property: string, offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => { if (placeholderLevel === nestingLevel - 1) { if (!placeHoldersForKey.get(messageKey!)!.some((literal: Literal, index: number, array: Literal[]) => literal.value === property)) { - showErrorAt(offset + 1, offset + property.length + 1, `Placeholder ${property} is being declared, but not used in message.`, vscode.DiagnosticSeverity.Warning); + showErrorAt(offset + 1, offset + property.length + 1, `Placeholder "${property}" is being declared, but not used in message.`, vscode.DiagnosticSeverity.Warning); } definedPlaceholders.push(property); decorateAt(offset + 1, offset + property.length + 1, argDecoration); @@ -116,7 +116,7 @@ export class DecoratorAndParser { const isGlobalMetadata = property.startsWith('@@'); const messageKeyExists = placeHoldersForKey.has(property.substring(1)); if (!isGlobalMetadata && !messageKeyExists) { - showErrorAt(propertyOffsetStart, propertyOffsetEnd, `Metadata for an undefined key. Add a message key with the name ${property.substring(1)}.`, vscode.DiagnosticSeverity.Error); + showErrorAt(propertyOffsetStart, propertyOffsetEnd, `Metadata for an undefined key. Add a message key with the name "${property.substring(1)}".`, vscode.DiagnosticSeverity.Error); } metadataLevel = nestingLevel; } else { @@ -124,7 +124,7 @@ export class DecoratorAndParser { messageKey = property; placeHoldersForKey.set(messageKey, []); } else { - showErrorAt(propertyOffsetStart, propertyOffsetEnd, `${property} is not a valid message key.`, vscode.DiagnosticSeverity.Error); + showErrorAt(propertyOffsetStart, propertyOffsetEnd, `Key "${property}" is not a valid message key.`, vscode.DiagnosticSeverity.Error); } } } @@ -138,7 +138,7 @@ export class DecoratorAndParser { placeholderLevel = null; for (const placeholder of placeHoldersForKey.get(messageKey!)!) { if (!definedPlaceholders.includes(placeholder.value)) { - showErrorAt(placeholder.start, placeholder.end, `Placeholder ${placeholder.value} not defined in the message metadata.`, vscode.DiagnosticSeverity.Warning); + showErrorAt(placeholder.start, placeholder.end, `Placeholder "${placeholder.value}" not defined in the message metadata.`, vscode.DiagnosticSeverity.Warning); } } definedPlaceholders = []; @@ -171,7 +171,7 @@ export class DecoratorAndParser { placeHoldersForKey.get(messageKey!)!.push(new Literal(part, partOffset, partOffsetEnd)); decorateAt(partOffset, partOffsetEnd, argDecoration); } else { - showErrorAt(partOffset, partOffsetEnd, 'This is not a valid argument name.', vscode.DiagnosticSeverity.Error); + showErrorAt(partOffset, partOffsetEnd, `"${part}" is not a valid argument name.`, vscode.DiagnosticSeverity.Error); } } else { decorateMessage(part, partOffset - 1, colorMap, editor, true); @@ -211,9 +211,9 @@ export class DecoratorAndParser { const range = new vscode.Range(editor.document.positionAt(start), editor.document.positionAt(end)); diagnosticsList.push(new vscode.Diagnostic(range, errorMessage, severity)); } - } - + return { diagnostics: diagnosticsList, decorations: decorationsMap }; + } } function matchCurlyBrackets(value: string) { return XRegExp.matchRecursive(value, '\\{', '\\}', 'g', { diff --git a/src/test/suite/extension.test.ts b/src/test/suite/extension.test.ts index 30e69ad..a4d7d69 100644 --- a/src/test/suite/extension.test.ts +++ b/src/test/suite/extension.test.ts @@ -12,17 +12,79 @@ // See the License for the specific language governing permissions and // limitations under the License. import * as assert from 'assert'; +import path = require('path'); +import { TextEncoder } from 'util'; // You can import and use all API from the 'vscode' module // as well as import your extension to test it import * as vscode from 'vscode'; -// import * as myExtension from '../../extension'; +import { argDecoration, selectDecoration, pluralDecoration, DecoratorAndParser } from '../../parseAndDecorate'; -suite('Extension Test Suite', () => { - vscode.window.showInformationMessage('Start all tests.'); +const annotationNames = new Map([ + [argDecoration, '[decoration]argument'], + [selectDecoration, '[decoration]select'], + [pluralDecoration, '[decoration]plural'], +]); - test('Sample test', () => { - assert.strictEqual(-1, [1, 2, 3].indexOf(5)); - assert.strictEqual(-1, [1, 2, 3].indexOf(0)); +suite('Extension Test Suite', async () => { + test("should annotate function with parameters", async () => { + const contentWithAnnotations = await buildContentWithAnnotations('testarb.arb'); + // const goldenEditor = await getEditor('testarb.annotated'); + + // assert.equal(contentWithAnnotations, goldenEditor.document.getText()); + await regenerateGolden(contentWithAnnotations); }); }); + +const testFolderLocation: string = "/../../../src/test/"; + +async function regenerateGolden(contentWithAnnotations: string) { + const uri = vscode.Uri.file(path.join(__dirname, testFolderLocation, 'testarb.annotated')); + await vscode.workspace.fs.writeFile(uri, new TextEncoder().encode(contentWithAnnotations)); +} + +async function buildContentWithAnnotations(filename: string) { + const editor = await getEditor(filename); + const decorations = new DecoratorAndParser().parseAndDecorate(editor); + const content = editor.document.getText(); + const annotationsForLine = new Map(); + for (const entry of decorations?.decorations.entries() ?? []) { + const decorationType = entry[0]; + for (const range of entry[1]) { + for (let lineNumber = range.start.line; lineNumber <= range.end.line; lineNumber++) { + const line = editor.document.lineAt(lineNumber); + const offsetInLine = range.start.character - line.range.start.character; + const lengthInLine = (line.range.end.character - range.start.character) - (line.range.end.character - range.end.character); + const annotation = ' '.repeat(offsetInLine) + '^'.repeat(lengthInLine) + annotationNames.get(decorationType)!; + annotationsForLine.set(lineNumber, [...(annotationsForLine.get(lineNumber) ?? []), annotation]); + } + } + } + for (const diagnostic of decorations?.diagnostics ?? []) { + const range = diagnostic.range; + for (let lineNumber = range.start.line; lineNumber <= range.end.line; lineNumber++) { + const line = editor.document.lineAt(lineNumber); + const offsetInLine = range.start.character - line.range.start.character; + const lengthInLine = (line.range.end.character - range.start.character) - (line.range.end.character - range.end.character); + const annotation = ' '.repeat(offsetInLine) + '^'.repeat(lengthInLine) + '[' + vscode.DiagnosticSeverity[diagnostic.severity] + ']:"' + diagnostic.message + '"'; + annotationsForLine.set(lineNumber, [...(annotationsForLine.get(lineNumber) ?? []), annotation]); + } + } + const lines = content.split('\n'); + const numLines = lines.length; + for (let index = numLines; index > 0; index--) { + if (annotationsForLine.has(index)) { + lines.splice(index + 1, 0, ...annotationsForLine.get(index)!); + } + } + const contentWithAnnotations = lines.join('\n'); + return contentWithAnnotations; +} + +async function getEditor(filename: string) { + const testFilePath = path.join(__dirname, testFolderLocation, filename); + const uri = vscode.Uri.file(testFilePath); + const document = await vscode.workspace.openTextDocument(uri); + const editor = await vscode.window.showTextDocument(document); + return editor; +} diff --git a/src/test/testarb.annotated b/src/test/testarb.annotated new file mode 100644 index 0000000..79289b5 --- /dev/null +++ b/src/test/testarb.annotated @@ -0,0 +1,92 @@ +{ + "@@locale": "en", + "appName": "Demo app", + "pageLog{inUsername": "Your username", + ^^^^^^^^^^^^^^^^^^[Error]:"Key "pageLog{inUsername" is not a valid message key." + "@pageLoginUsername": {}, + ^^^^^^^^^^^^^^^^^^[Error]:"Metadata for an undefined key. Add a message key with the name "pageLoginUsername"." + "pageLoginPassword": "Your password", + "@pageLoginPassword": {}, + "pageHomeTitle": "Welcome {firstName}", + ^^^^^^^^^[decoration]argument + "@pageHomeTitle": { + "description": "Welcome message on the Home screen", + "placeholders": { + "firstName": {} + ^^^^^^^^^[decoration]argument + } + }, + "pageHomeInboxCount": "{count, plural, zero{I have {vehicleType, select, sedn{Sedan} cabrolet{Solid roof cabriolet} tuck{16 wheel truck} oter{Other}} no new messages} one{You have 1 new message} other{You have {count} new messages}}", + ^^^^^[decoration]argument + ^^^^^^^^^^^[decoration]argument + ^^^^^[decoration]argument + ^^^^^[decoration]select + ^^^^^^^^[decoration]select + ^^^^[decoration]select + ^^^^[decoration]select + ^^^^^[decoration]plural + ^^^[decoration]plural + ^^^^^[decoration]plural + "@pageHomeInboxCount": { + "description": "New messages count on the Home screen", + "placeholders": { + "count": {}, + ^^^^^[decoration]argument + "vehicleType": {} + ^^^^^^^^^^^[decoration]argument + } + }, + "pageHomeBirthday": "Today is {sex, select, male{his b'{irthday} female{her birthday} other{their birthday}}.", + ^^^[decoration]argument + ^^^^^[decoration]select + ^^^^^^[decoration]select + ^^^^^[decoration]select + "@pageHomeBirthday": { + "description": "Birthday message on the Home screen", + "placeholders": { + "sex": {} + ^^^[decoration]argument + } + }, + "commonVehicleType": "{vehicleType, s{elect, sedan{Sedan} cabriolet{Solid roof cabriolet} truck{16 wheel truck} other{Other}}", + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^[Error]:"Unbalanced curly bracket found. Try escaping the bracket using a single quote ' ." + "@commonVeshicleType": { + ^^^^^^^^^^^^^^^^^^^[Error]:"Metadata for an undefined key. Add a message key with the name "commonVeshicleType"." + "description": "Vehicle type", + "placeholders": { + "vehicleType": {} + ^^^^^^^^^^^[decoration]argument + ^^^^^^^^^^^[Warning]:"Placeholder "vehicleType" is being declared, but not used in message." + } + }, + "pageHomeBalance": "Your balance is {am[ount} on {date2}", + ^^^^^[decoration]argument + ^^^^^^^[Error]:""am[ount" is not a valid argument name." + ^^^^^[Warning]:"Placeholder "date2" not defined in the message metadata." + "@pageHomeBalance": { + "placeholders": { + "amount": { + ^^^^^^[decoration]argument + ^^^^^^[Warning]:"Placeholder "amount" is being declared, but not used in message." + "type": "double", + "format": "currency", + "example": "$1000.00", + "description": "Account balance", + "optionalParameters": { + "decimalDigits": 2, + "name": "USD", + "symbol": "$", + "customPattern": "ยค#0.00" + } + }, + "date": { + ^^^^[decoration]argument + ^^^^[Warning]:"Placeholder "date" is being declared, but not used in message." + "type": "DateTime", + "format": "yMd", + "example": "11/10/2021", + "description": "Balance date" + } + } + } +} \ No newline at end of file diff --git a/test/testarb.arb b/src/test/testarb.arb similarity index 100% rename from test/testarb.arb rename to src/test/testarb.arb