Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .npmrc
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
registry=https://registry.npmjs.org/
registry=https://office.pkgs.visualstudio.com/_packaging/OfficeDev/npm/registry/
always-auth=true
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ Custom functions enable you to add new functions to Excel by defining those func

This repository contains the source code used by the [Yo Office generator](https://github.com/OfficeDev/generator-office) when you create a new custom functions project. You can also use this repository as a sample to base your own custom functions project from if you choose not to use the generator. For more detailed information about custom functions in Excel, see the [Custom functions overview](https://learn.microsoft.com/office/dev/add-ins/excel/custom-functions-overview) article in the Office Add-ins documentation or see the [additional resources](#additional-resources) section of this repository.

## npm registry auth
Run the following command to get the authenticator for the first time:

`npm install --global @microsoft/artifacts-npm-credprovider --registry https://pkgs.dev.azure.com/artifacts-public/23934c1b-a3b5-4b70-9dd3-d1bef4cc72a0/_packaging/AzureArtifacts/npm/registry/`
Comment thread
millerds marked this conversation as resolved.

Run `artifacts-npm-credprovider` to authenticate for the npm registry.

## Debugging custom functions

This template supports debugging custom functions from [Visual Studio Code](https://code.visualstudio.com/). For more information see [Custom functions debugging](https://aka.ms/custom-functions-debug). For general information on debugging task panes and other Office Add-in parts, see [Test and debug Office Add-ins](https://learn.microsoft.com/office/dev/add-ins/testing/test-debug-office-add-ins).
Expand Down
4,845 changes: 2,455 additions & 2,390 deletions package-lock.json

Large diffs are not rendered by default.

25 changes: 12 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,31 +44,30 @@
"@types/office-runtime": "^1.0.35",
"acorn": "^8.5.0",
"babel-loader": "^10.1.1",
"copy-webpack-plugin": "^12.0.2",
"copy-webpack-plugin": "^14.0.0",
"custom-functions-metadata-plugin": "^2.1.2",
"eslint-plugin-office-addins": "^4.0.3",
"eslint-plugin-office-addins": "^4.0.10",
"file-loader": "^6.2.0",
"html-loader": "^5.0.0",
"html-webpack-plugin": "^5.6.0",
"mocha": "^11.7.4",
"office-addin-cli": "^2.0.6",
"office-addin-debugging": "^6.0.6",
"office-addin-dev-certs": "^2.0.6",
"office-addin-lint": "^3.0.6",
"office-addin-manifest": "^2.1.2",
"office-addin-mock": "^3.0.6",
"office-addin-prettier-config": "^2.0.1",
"office-addin-test-helpers": "^2.0.3",
"office-addin-test-server": "^2.0.3",
"office-addin-cli": "^2.0.10",
"office-addin-debugging": "^6.1.2",
"office-addin-dev-certs": "^2.0.10",
"office-addin-lint": "^3.0.10",
"office-addin-manifest": "^2.1.6",
"office-addin-mock": "^3.0.10",
"office-addin-prettier-config": "^2.0.5",
"office-addin-test-helpers": "^2.0.7",
"office-addin-test-server": "^2.0.7",
"os-browserify": "^0.3.0",
"process": "^0.11.10",
"request": "^2.88.2",
"source-map-loader": "^5.0.0",
"ts-node": "^10.9.2",
"typescript": "^5.4.2",
"webpack": "^5.105.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "5.2.5"
"webpack-dev-server": "^6.0.0"
},
"prettier": "office-addin-prettier-config",
"browserslist": [
Expand Down
2 changes: 1 addition & 1 deletion src/commands/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function action(event: Office.AddinCommands.Event) {
};

// Show a notification message.
Office.context.mailbox.item.notificationMessages.replaceAsync(
Office.context.mailbox.item?.notificationMessages.replaceAsync(
"ActionPerformanceNotification",
message
);
Expand Down
8 changes: 4 additions & 4 deletions src/taskpane/taskpane.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@

// The initialize function must be run each time a new page is loaded
Office.onReady(() => {
document.getElementById("sideload-msg").style.display = "none";
document.getElementById("app-body").style.display = "flex";
document.getElementById("run").onclick = run;
document.getElementById("sideload-msg")!.style.display = "none";
document.getElementById("app-body")!.style.display = "flex";
document.getElementById("run")!.onclick = run;
});

export async function run() {
try {
await Excel.run(async (context) => {
await Excel.run(async (context: Excel.RequestContext) => {
/**
* Insert your Excel code here
*/
Expand Down
25 changes: 14 additions & 11 deletions test/end-to-end/src/debugger-websocket.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
import * as assert from "assert";
import { sleep } from "./test-helpers";
const WebSocket = require("ws");
const request = require("request");

/* global require, console */
let connectionOpened = false;
let messageId = 0;
const limitOfReconnectTries = 60;
let wsUrl: string | undefined;

function findUrl(jsonUrl: string): void {
let options = { json: true };

request(jsonUrl, options, (error, res, body) => {
if (!error && res.statusCode == 200) {
wsUrl = body[0].webSocketDebuggerUrl;
async function findUrl(jsonUrl: string): Promise<void> {
try {
const response = await fetch(jsonUrl, { signal: AbortSignal.timeout(1000) });
if (!response.ok) {
return;
}
});

const body = await response.json();
wsUrl = body?.[0]?.webSocketDebuggerUrl;
} catch {
// Debugger endpoint may not be ready yet. Retry loop handles this.
}
}

export async function connectToWebsocket(reconnectTry: number = 1): Promise<WebSocket | undefined> {
Expand All @@ -25,7 +28,7 @@ export async function connectToWebsocket(reconnectTry: number = 1): Promise<WebS

while (!wsUrl && reconnectTry < limitOfReconnectTries) {
console.log(`Attaching debugger to '${jsonUrl}'...`);
findUrl(jsonUrl);
await findUrl(jsonUrl);
reconnectTry++;
await sleep(1000);
}
Expand All @@ -39,12 +42,12 @@ export async function connectToWebsocket(reconnectTry: number = 1): Promise<WebS
connectionOpened = true;
return resolve(ws);
};
ws.onerror = (err) => {
ws.onerror = (err: any) => {
if (connectionOpened) {
assert.fail(`Websocket error: ${err.message}`);
}
};
ws.onmessage = (response) => {
ws.onmessage = (response: any) => {
assert.strictEqual(
JSON.parse(response.data).error,
undefined,
Expand Down
62 changes: 55 additions & 7 deletions test/end-to-end/src/test-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as childProcess from "child_process";
export async function closeWorkbook(): Promise<void> {
await sleep(3000); // wait for host to settle
try {
await Excel.run(async (context) => {
await Excel.run(async (context: Excel.RequestContext) => {
// @ts-ignore
context.workbook.close(Excel.CloseBehavior.skipSave);
Promise.resolve();
Expand All @@ -15,12 +15,60 @@ export async function closeWorkbook(): Promise<void> {
}
Comment thread
millerds marked this conversation as resolved.
}

export function addTestResult(testValues: any[], resultName: string, resultValue: any, expectedValue: any) {
var data = {};
data["expectedValue"] = expectedValue;
data["resultName"] = resultName;
data["resultValue"] = resultValue;
testValues.push(data);
export function addTestResult(testValues: any[], name: string, value: any, expectedValue: any) {
testValues.push({
expectedValue,
name,
value,
});
}

export function addErrorResult(testValues: any[], errorMessage: string) {
testValues.push({
expectedValue: "no-error",
name: "test-error",
value: errorMessage,
});
}

export function formatError(err: any): string {
if (!err) {
return "Unknown error (null/undefined)";
}

const parts: string[] = [];

if (err.message) {
parts.push(`Message: ${err.message}`);
} else {
parts.push(`${err}`);
}

if (err.code) {
parts.push(`Code: ${err.code}`);
}

if (err.debugInfo) {
if (err.debugInfo.code) {
parts.push(`DebugCode: ${err.debugInfo.code}`);
}
if (err.debugInfo.message) {
parts.push(`DebugMessage: ${err.debugInfo.message}`);
}
if (err.debugInfo.errorLocation) {
parts.push(`Location: ${err.debugInfo.errorLocation}`);
}
if (err.debugInfo.innerError) {
const inner = err.debugInfo.innerError;
parts.push(`InnerError: ${inner.code || ""} ${inner.message || JSON.stringify(inner)}`);
}
}

if (err.stack) {
parts.push(`Stack: ${err.stack}`);
}

return parts.join(" | ");
}

export async function closeDesktopApplication(): Promise<boolean> {
Expand Down
4 changes: 2 additions & 2 deletions test/end-to-end/src/test-taskpane.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<!-- This file shows how to design a first-run page that provides a welcome screen to the user about the features of the add-in. -->

<!DOCTYPE html>
<html>
<html lang="en">

<head>
<meta charset="UTF-8" />
Expand All @@ -28,7 +28,7 @@ <h1 class="ms-font-su">Testing</h1>
<section id="sideload-msg" class="ms-welcome__main">
<h2 class="ms-font-xl">Please sideload your add-in to see app body.</h2>
</section>
<main id="app-body" class="ms-welcome__main" style="display: none;">
<main id="app-body" class="ms-welcome__main">
Comment thread
millerds marked this conversation as resolved.
<h2 class="ms-font-xl"> Discover what Office Add-ins can do for you today! </h2>
<ul class="ms-List ms-welcome__features">
<li class="ms-ListItem">
Expand Down
110 changes: 78 additions & 32 deletions test/end-to-end/src/test-taskpane.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,115 @@
import functionsJsonData from "./test-data.json";
import { pingTestServer, sendTestResults } from "office-addin-test-helpers";
import { closeWorkbook, sleep } from "./test-helpers";
import { addErrorResult, closeWorkbook, formatError, sleep } from "./test-helpers";

/* global Office, document, Excel, run, navigator */
const customFunctionsData = (<any>functionsJsonData).functions;
const port: number = 4201;
let testValues = [];
let testValues: any[] = [];

Office.onReady(async () => {
document.getElementById("sideload-msg").style.display = "none";
document.getElementById("app-body").style.display = "flex";
document.getElementById("run").onclick = run;
document.getElementById("sideload-msg")!.style.display = "none";
document.getElementById("app-body")!.style.display = "flex";
document.getElementById("run")!.onclick = run;
addTestResult("UserAgent", navigator.userAgent);

const testServerResponse: object = await pingTestServer(port);
if (testServerResponse["status"] === 200) {
try {
const testServerResponse = (await pingTestServer(port)) as { status?: number };
if (testServerResponse.status === 200) {
await runTest();
} else {
addErrorResult(testValues, `Ping failed: ${JSON.stringify(testServerResponse)}`);
await sendTestResults(testValues, port).catch(() => {});
}
} catch (err) {
addErrorResult(testValues, `Initialization failed: ${formatError(err)}`);
await sendTestResults(testValues, port).catch(() => {});
}
});

async function runTest(): Promise<void> {
try {
await runCfTests();
await sendTestResults(testValues, port);
await closeWorkbook();
Comment thread
millerds marked this conversation as resolved.
} catch (err) {
testValues = [];
addErrorResult(testValues, `runTest failed: ${formatError(err)}`);
await sendTestResults(testValues, port).catch(() => {});
}
});
}

async function runCfTests(): Promise<void> {
// Exercise custom functions
await Excel.run(async (context) => {
for (let key in customFunctionsData) {
const formula: string = customFunctionsData[key].formula;
const range = context.workbook.getSelectedRange();
range.formulas = [[formula]];
await context.sync();
for (let key in customFunctionsData) {
const formula: string = customFunctionsData[key].formula;
const readCount: number = customFunctionsData[key].streaming != undefined ? 2 : 1;
let capturedValues: any[] = [];

for (let attempt = 0; attempt < 3; attempt++) {
await Excel.run(async (context: Excel.RequestContext) => {
const range = context.workbook.getSelectedRange();
range.formulas = [[formula]];
await context.sync();
});

await sleep(5000);

// Check to if this is a streaming function
await readCFData(key, customFunctionsData[key].streaming != undefined ? 2 : 1);
capturedValues = await readCFData(readCount);
const hasCalcError = capturedValues.some((value) => typeof value === "string" && value.includes("#CALC!"));
if (!hasCalcError) {
break;
}

// Re-enter formula when custom function registration/evaluation is still warming up.
await sleep(2000);
}
});

for (const value of capturedValues) {
addTestResult(key, value);
}
}
}

export async function readCFData(cfName: string, readCount: number): Promise<void> {
await Excel.run(async (context) => {
export async function readCFData(readCount: number): Promise<any[]> {
return Excel.run(async (context: Excel.RequestContext) => {
const capturedValues: any[] = [];

// if this is a streaming function, we want to capture two values so we can
// validate the function is indeed streaming
for (let i = 0; i < readCount; i++) {
try {
const range = context.workbook.getSelectedRange();
if (i > 0) {
// For streaming functions, wait for the next emitted value.
await sleep(5000);
}

const range = context.workbook.getSelectedRange();
let value: any = undefined;

// Retry a few times when Excel still reports transient calculation errors.
for (let retry = 0; retry < 4; retry++) {
range.load("values");
await context.sync();
value = range.values?.[0]?.[0];

await sleep(5000);
if (typeof value !== "string" || !value.includes("#CALC!")) {
break;
}

addTestResult(cfName, range.values[0][0]);
Promise.resolve();
} catch {
Promise.reject();
await sleep(2000);
}

capturedValues.push(value);
}

return capturedValues;
});
}

function addTestResult(resultName: string, resultValue: any) {
var data = {};
var nameKey = "Name";
var valueKey = "Value";
data[nameKey] = resultName;
data[valueKey] = resultValue;
function addTestResult(name: string, value: any) {
const data = {
name: name,
value: value,
};
testValues.push(data);
}
Loading