miro-export 1.1.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/build.yml +4 -4
- package/.github/workflows/publish.yml +3 -3
- package/.github/workflows/test.yml +6 -4
- package/README.md +4 -3
- package/build/cli.js +65 -49
- package/build/index.d.ts +26 -0
- package/build/index.js +65 -15
- package/package.json +16 -16
- package/src/cli.ts +86 -63
- package/src/index.ts +115 -20
- package/src/miro-runtime.d.ts +2 -0
- package/src/miro-types.ts +19 -17
- package/tests/api.test.ts +41 -0
|
@@ -8,11 +8,11 @@ jobs:
|
|
|
8
8
|
build:
|
|
9
9
|
runs-on: ubuntu-latest
|
|
10
10
|
steps:
|
|
11
|
-
- uses: actions/checkout@
|
|
12
|
-
- uses: pnpm/action-setup@
|
|
11
|
+
- uses: actions/checkout@v6
|
|
12
|
+
- uses: pnpm/action-setup@v4
|
|
13
13
|
with:
|
|
14
14
|
version: 8
|
|
15
|
-
- uses: actions/setup-node@
|
|
15
|
+
- uses: actions/setup-node@v6
|
|
16
16
|
with:
|
|
17
17
|
node-version: "22.x"
|
|
18
18
|
- name: Install dependencies
|
|
@@ -22,7 +22,7 @@ jobs:
|
|
|
22
22
|
- name: Build
|
|
23
23
|
run: pnpm run build
|
|
24
24
|
- name: Upload Artifact
|
|
25
|
-
uses: actions/upload-artifact@
|
|
25
|
+
uses: actions/upload-artifact@v6
|
|
26
26
|
with:
|
|
27
27
|
name: build
|
|
28
28
|
path: build/
|
|
@@ -8,11 +8,11 @@ jobs:
|
|
|
8
8
|
publish:
|
|
9
9
|
runs-on: ubuntu-latest
|
|
10
10
|
steps:
|
|
11
|
-
- uses: actions/checkout@
|
|
12
|
-
- uses: pnpm/action-setup@
|
|
11
|
+
- uses: actions/checkout@v6
|
|
12
|
+
- uses: pnpm/action-setup@v4
|
|
13
13
|
with:
|
|
14
14
|
version: 8
|
|
15
|
-
- uses: actions/setup-node@
|
|
15
|
+
- uses: actions/setup-node@v6
|
|
16
16
|
with:
|
|
17
17
|
node-version: "22.x"
|
|
18
18
|
registry-url: "https://registry.npmjs.org"
|
|
@@ -6,13 +6,13 @@ on:
|
|
|
6
6
|
|
|
7
7
|
jobs:
|
|
8
8
|
test:
|
|
9
|
-
runs-on: ubuntu-
|
|
9
|
+
runs-on: ubuntu-22.04
|
|
10
10
|
steps:
|
|
11
|
-
- uses: actions/checkout@
|
|
12
|
-
- uses: pnpm/action-setup@
|
|
11
|
+
- uses: actions/checkout@v6
|
|
12
|
+
- uses: pnpm/action-setup@v4
|
|
13
13
|
with:
|
|
14
14
|
version: 8
|
|
15
|
-
- uses: actions/setup-node@
|
|
15
|
+
- uses: actions/setup-node@v6
|
|
16
16
|
with:
|
|
17
17
|
node-version: "22.x"
|
|
18
18
|
- name: Install dependencies
|
|
@@ -21,3 +21,5 @@ jobs:
|
|
|
21
21
|
run: pnpm run test
|
|
22
22
|
env:
|
|
23
23
|
TEST_BOARD_ID: ${{secrets.TEST_BOARD_ID}}
|
|
24
|
+
PRIVATE_TEST_BOARD_ID: ${{secrets.PRIVATE_TEST_BOARD_ID}}
|
|
25
|
+
BUGGY_TEST_BOARD_ID: ${{secrets.BUGGY_TEST_BOARD_ID}}
|
package/README.md
CHANGED
|
@@ -30,8 +30,9 @@ Options:
|
|
|
30
30
|
-t, --token <token> Miro token
|
|
31
31
|
-b, --board-id <boardId> The board ID
|
|
32
32
|
-f, --frame-names <frameNames...> The frame name(s), leave empty to export entire board
|
|
33
|
-
-o, --output-file <filename> A file to output the SVG to (stdout if not
|
|
34
|
-
-e, --export-format <format> 'svg' or 'json' (default:
|
|
33
|
+
-o, --output-file <filename> A file to output the SVG to (stdout if not specified)
|
|
34
|
+
-e, --export-format <format> 'svg' or 'json' (default: "svg")
|
|
35
|
+
-l, --load-timeout <milliseconds> Timeout for loading the board in milliseconds (default: 15000)
|
|
35
36
|
-h, --help display help for command
|
|
36
37
|
```
|
|
37
38
|
|
|
@@ -116,7 +117,7 @@ const svgOfFrame1 = await miroBoard.getSvg([framesWithTitleFrame1[0].id]);
|
|
|
116
117
|
> [!WARNING]
|
|
117
118
|
> Remember to dispose the instance to make sure the browser is closed and the process
|
|
118
119
|
> can exit. `await using` (as shown above) does this automatically, but is not supported
|
|
119
|
-
> in all environments and may not be the optimal
|
|
120
|
+
> in all environments and may not be the optimal choice in every case. Alternatively,
|
|
120
121
|
> `miroBoard.dispose()` may be called at any time to dispose of the instance manually.
|
|
121
122
|
|
|
122
123
|
Types for many of the common board object types has been provided in [miro-types.ts](src/miro-types.ts).
|
package/build/cli.js
CHANGED
|
@@ -54,69 +54,85 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
54
54
|
import { writeFile } from "fs/promises";
|
|
55
55
|
import { program } from "@commander-js/extra-typings";
|
|
56
56
|
import { MiroBoard } from "./index.js";
|
|
57
|
-
const { token, boardId, frameNames, outputFile, exportFormat } = program
|
|
57
|
+
const { token, boardId, frameNames, outputFile, exportFormat, loadTimeout } = program
|
|
58
58
|
.option("-t, --token <token>", "Miro token")
|
|
59
59
|
.requiredOption("-b, --board-id <boardId>", "The board ID")
|
|
60
60
|
.option("-f, --frame-names <frameNames...>", "The frame name(s), leave empty to export entire board")
|
|
61
|
-
.option("-o, --output-file <filename>", "A file to output the SVG to (stdout if not
|
|
62
|
-
.option("-e, --export-format <format>", "'svg' or 'json'
|
|
61
|
+
.option("-o, --output-file <filename>", "A file to output the SVG to (stdout if not specified)")
|
|
62
|
+
.option("-e, --export-format <format>", "'svg' or 'json'", "svg")
|
|
63
|
+
.option("-l, --load-timeout <milliseconds>", "Timeout for loading the board in milliseconds", (arg) => parseInt(arg, 10), 15000)
|
|
63
64
|
.parse()
|
|
64
65
|
.opts();
|
|
65
66
|
(async () => {
|
|
66
|
-
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
67
67
|
try {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
69
|
+
try {
|
|
70
|
+
const miroBoard = __addDisposableResource(env_1, new MiroBoard({
|
|
71
|
+
token,
|
|
72
|
+
boardId,
|
|
73
|
+
boardLoadTimeoutMs: loadTimeout
|
|
74
|
+
}), true);
|
|
75
|
+
async function getFrames(frameNames) {
|
|
76
|
+
const frames = await miroBoard.getBoardObjects({ type: "frame" }, { title: frameNames });
|
|
77
|
+
if (frames && frames.length !== frameNames.length) {
|
|
78
|
+
throw Error(`${frameNames.length - frames.length} frame(s) could not be found on the board.`);
|
|
79
|
+
}
|
|
80
|
+
return frames;
|
|
73
81
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
async function getSvg(frames) {
|
|
77
|
-
return await miroBoard.getSvg(frames?.map(({ id }) => id).filter((id) => !!id));
|
|
78
|
-
}
|
|
79
|
-
async function getJson(frames) {
|
|
80
|
-
if (frames) {
|
|
81
|
-
const frameChildren = await miroBoard.getBoardObjects({
|
|
82
|
-
id: frames.flatMap((frame) => frame.childrenIds)
|
|
83
|
-
});
|
|
84
|
-
const groupChildren = await miroBoard.getBoardObjects({
|
|
85
|
-
id: frameChildren
|
|
86
|
-
.filter((child) => child.type === "group")
|
|
87
|
-
.flatMap((child) => child.itemsIds)
|
|
88
|
-
});
|
|
89
|
-
return JSON.stringify([...frames, ...frameChildren, ...groupChildren]);
|
|
82
|
+
async function getSvg(frames) {
|
|
83
|
+
return await miroBoard.getSvg(frames?.map(({ id }) => id).filter((id) => !!id));
|
|
90
84
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
85
|
+
async function getJson(frames) {
|
|
86
|
+
if (frames) {
|
|
87
|
+
const frameChildren = await miroBoard.getBoardObjects({
|
|
88
|
+
id: frames.flatMap((frame) => frame.childrenIds)
|
|
89
|
+
});
|
|
90
|
+
const groupChildren = await miroBoard.getBoardObjects({
|
|
91
|
+
id: frameChildren
|
|
92
|
+
.filter((child) => child.type === "group")
|
|
93
|
+
.flatMap((child) => child.itemsIds)
|
|
94
|
+
});
|
|
95
|
+
return JSON.stringify([...frames, ...frameChildren, ...groupChildren]);
|
|
96
|
+
}
|
|
97
|
+
return JSON.stringify(await miroBoard.getBoardObjects({}));
|
|
101
98
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
99
|
+
const getFn = exportFormat === "json" ? getJson : getSvg;
|
|
100
|
+
if (outputFile?.includes("{frameName}")) {
|
|
101
|
+
if (!frameNames) {
|
|
102
|
+
throw Error("Expected frame names to be given when the output file name format expects a frame name.");
|
|
103
|
+
}
|
|
104
|
+
for (const frameName of frameNames) {
|
|
105
|
+
const output = await getFn(await getFrames([frameName]));
|
|
106
|
+
await writeFile(outputFile.replace("{frameName}", frameName), output);
|
|
107
|
+
}
|
|
107
108
|
}
|
|
108
109
|
else {
|
|
109
|
-
|
|
110
|
+
const svg = await getFn(frameNames && (await getFrames(frameNames)));
|
|
111
|
+
if (outputFile) {
|
|
112
|
+
await writeFile(outputFile, svg);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
process.stdout.write(svg);
|
|
116
|
+
}
|
|
110
117
|
}
|
|
111
118
|
}
|
|
119
|
+
catch (e_1) {
|
|
120
|
+
env_1.error = e_1;
|
|
121
|
+
env_1.hasError = true;
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
const result_1 = __disposeResources(env_1);
|
|
125
|
+
if (result_1)
|
|
126
|
+
await result_1;
|
|
127
|
+
}
|
|
112
128
|
}
|
|
113
|
-
catch (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
129
|
+
catch (err) {
|
|
130
|
+
const RED = "\x1b[31m";
|
|
131
|
+
const RESET = "\x1b[0m";
|
|
132
|
+
const GRAY = "\x1b[38;5;248m";
|
|
133
|
+
console.error(`❌ ${RED}Error:${RESET} ${err instanceof Error ? err.message : err}`);
|
|
134
|
+
console.error(GRAY);
|
|
135
|
+
console.error(err);
|
|
136
|
+
console.error(RESET);
|
|
121
137
|
}
|
|
122
138
|
})();
|
package/build/index.d.ts
CHANGED
|
@@ -1,8 +1,27 @@
|
|
|
1
|
+
import { type LaunchOptions } from "puppeteer";
|
|
1
2
|
import type { BoardObject } from "./miro-types.ts";
|
|
2
3
|
import type { GetBoardsFilter } from "./miro-runtime.ts";
|
|
3
4
|
interface InitialMiroBoardOptions {
|
|
5
|
+
/**
|
|
6
|
+
* The Miro authentication token with access to load the board.
|
|
7
|
+
* Optional if anonymous users may access the board without
|
|
8
|
+
* logging in.
|
|
9
|
+
*/
|
|
4
10
|
token?: string;
|
|
11
|
+
/**
|
|
12
|
+
* The Miro board ID.
|
|
13
|
+
*/
|
|
5
14
|
boardId: string;
|
|
15
|
+
/**
|
|
16
|
+
* Optional custom Puppeteer launch options.
|
|
17
|
+
*/
|
|
18
|
+
puppeteerOptions?: LaunchOptions;
|
|
19
|
+
/**
|
|
20
|
+
* Timeout until it is determined that the Miro board could
|
|
21
|
+
* not be loaded for some reason, in milliseconds. Default
|
|
22
|
+
* is 15 seconds (15,000 milliseconds).
|
|
23
|
+
*/
|
|
24
|
+
boardLoadTimeoutMs?: number;
|
|
6
25
|
}
|
|
7
26
|
type AdditionalFilter<T> = Partial<T> | Partial<{
|
|
8
27
|
[K in keyof T]: T[K][];
|
|
@@ -12,12 +31,19 @@ type FilteredResultsByType<F extends string | string[] | undefined, T> = F exten
|
|
|
12
31
|
}>[] : T[];
|
|
13
32
|
export declare class MiroBoard {
|
|
14
33
|
private context;
|
|
34
|
+
private isDisposed;
|
|
15
35
|
constructor(options: InitialMiroBoardOptions);
|
|
16
36
|
private initialize;
|
|
17
37
|
dispose(): Promise<void>;
|
|
18
38
|
[Symbol.asyncDispose](): Promise<void>;
|
|
19
39
|
private get browser();
|
|
20
40
|
private get page();
|
|
41
|
+
/**
|
|
42
|
+
* Checks that all widgets have been loaded. Times out to 5 seconds due to bug
|
|
43
|
+
* in Miro where certain boards fail to load all widgets if SDK is not required
|
|
44
|
+
* for this function call.
|
|
45
|
+
*/
|
|
46
|
+
private checkAllWidgetsLoaded;
|
|
21
47
|
getBoardObjects<F extends GetBoardsFilter>(filter: F, additionalFilter?: AdditionalFilter<BoardObject>): Promise<FilteredResultsByType<F["type"], BoardObject>>;
|
|
22
48
|
getSvg(objectsIds?: string[]): Promise<string>;
|
|
23
49
|
}
|
package/build/index.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import puppeteer, { Browser, Page } from "puppeteer";
|
|
2
|
+
const DEFAULT_BOARD_LOAD_TIMEOUT_MS = 15_000;
|
|
2
3
|
export class MiroBoard {
|
|
3
4
|
context = Promise.withResolvers();
|
|
5
|
+
isDisposed = false;
|
|
4
6
|
constructor(options) {
|
|
5
7
|
this.initialize(options);
|
|
6
8
|
}
|
|
7
9
|
async initialize(options) {
|
|
8
|
-
const browser = await puppeteer.launch({
|
|
10
|
+
const browser = await puppeteer.launch({
|
|
11
|
+
headless: true,
|
|
12
|
+
...(options.puppeteerOptions ?? {})
|
|
13
|
+
});
|
|
9
14
|
const page = await browser.newPage();
|
|
10
15
|
if (options.token) {
|
|
11
16
|
await browser.browserContexts()[0].setCookie({
|
|
@@ -23,24 +28,41 @@ export class MiroBoard {
|
|
|
23
28
|
await page.goto(`https://miro.com/app/board/${options.boardId}/`, {
|
|
24
29
|
waitUntil: "domcontentloaded"
|
|
25
30
|
});
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
let miroValue;
|
|
31
|
-
Object.defineProperty(window, "miro", {
|
|
32
|
-
get() {
|
|
33
|
-
return miroValue;
|
|
34
|
-
},
|
|
35
|
-
set(value) {
|
|
36
|
-
miroValue = value;
|
|
31
|
+
try {
|
|
32
|
+
await page.evaluate((timeoutDuration) => new Promise((resolve, reject) => {
|
|
33
|
+
if (window.cmd?.board?.api) {
|
|
37
34
|
resolve();
|
|
38
35
|
}
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
const timeout = setTimeout(() => {
|
|
37
|
+
reject(new Error(`Miro board could not be loaded: application instance not available after ${timeoutDuration} ms. Check your network connection, access token and board access.`));
|
|
38
|
+
}, timeoutDuration);
|
|
39
|
+
const interval = setInterval(() => {
|
|
40
|
+
if (document.querySelector('[data-testid="signup-popup-container"]')) {
|
|
41
|
+
clearInterval(interval);
|
|
42
|
+
clearTimeout(timeout);
|
|
43
|
+
reject(new Error(`Miro board requires authentication. Check board access settings to allow anonymous access or supply a token.`));
|
|
44
|
+
}
|
|
45
|
+
else if (window.cmd?.board?.api) {
|
|
46
|
+
clearTimeout(timeout);
|
|
47
|
+
clearInterval(interval);
|
|
48
|
+
resolve();
|
|
49
|
+
}
|
|
50
|
+
}, 250);
|
|
51
|
+
}), options.boardLoadTimeoutMs ?? DEFAULT_BOARD_LOAD_TIMEOUT_MS);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
await browser.close();
|
|
55
|
+
this.context.reject(err);
|
|
56
|
+
this.isDisposed = true;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
41
59
|
this.context.resolve({ browser, page });
|
|
42
60
|
}
|
|
43
61
|
async dispose() {
|
|
62
|
+
if (this.isDisposed) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
this.isDisposed = true;
|
|
44
66
|
await (await this.browser).close();
|
|
45
67
|
}
|
|
46
68
|
async [Symbol.asyncDispose]() {
|
|
@@ -52,7 +74,34 @@ export class MiroBoard {
|
|
|
52
74
|
get page() {
|
|
53
75
|
return this.context.promise.then(({ page }) => page);
|
|
54
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Checks that all widgets have been loaded. Times out to 5 seconds due to bug
|
|
79
|
+
* in Miro where certain boards fail to load all widgets if SDK is not required
|
|
80
|
+
* for this function call.
|
|
81
|
+
*/
|
|
82
|
+
async checkAllWidgetsLoaded(mustHaveSdk = false) {
|
|
83
|
+
await (await this.page).evaluate((mustHaveSdk, timeoutDuration) => new Promise((resolve, reject) => {
|
|
84
|
+
const startTime = Date.now();
|
|
85
|
+
const interval = setInterval(() => {
|
|
86
|
+
if (window.cmd?.board?.api?.isAllWidgetsLoaded() &&
|
|
87
|
+
(!mustHaveSdk || window.miro)) {
|
|
88
|
+
clearInterval(interval);
|
|
89
|
+
resolve();
|
|
90
|
+
}
|
|
91
|
+
if (Date.now() - startTime >= timeoutDuration) {
|
|
92
|
+
clearInterval(interval);
|
|
93
|
+
if (mustHaveSdk) {
|
|
94
|
+
reject(new Error(`Miro SDK failed to load in ${timeoutDuration} ms. This is likely caused by a Miro-internal issue. Check that the board is accessible using an incognito browser window.`));
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
resolve();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}, 150);
|
|
101
|
+
}), mustHaveSdk, 3_000);
|
|
102
|
+
}
|
|
55
103
|
async getBoardObjects(filter, additionalFilter) {
|
|
104
|
+
await this.checkAllWidgetsLoaded(true);
|
|
56
105
|
return (await this.page).evaluate(async (filter, additionalFilter) => {
|
|
57
106
|
// @ts-expect-error - https://github.com/evanw/esbuild/issues/2605#issuecomment-2050808084
|
|
58
107
|
window.__name = (func) => func;
|
|
@@ -81,8 +130,9 @@ export class MiroBoard {
|
|
|
81
130
|
}, filter, additionalFilter);
|
|
82
131
|
}
|
|
83
132
|
async getSvg(objectsIds) {
|
|
133
|
+
await this.checkAllWidgetsLoaded(objectsIds !== undefined);
|
|
84
134
|
return (await this.page).evaluate(async (objectsIds) => {
|
|
85
|
-
|
|
135
|
+
window.cmd.board.api.clearSelection();
|
|
86
136
|
if (objectsIds) {
|
|
87
137
|
for (const id of objectsIds) {
|
|
88
138
|
await window.miro.board.select({ id });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "miro-export",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"author": "jolle <npm-contact@jolle.io>",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -23,25 +23,25 @@
|
|
|
23
23
|
"miro-export": "./build/cli.js"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@eslint/eslintrc": "^3.
|
|
27
|
-
"@eslint/js": "^9.
|
|
28
|
-
"@stylistic/eslint-plugin-ts": "^
|
|
29
|
-
"@types/node": "^
|
|
26
|
+
"@eslint/eslintrc": "^3.3.3",
|
|
27
|
+
"@eslint/js": "^9.39.2",
|
|
28
|
+
"@stylistic/eslint-plugin-ts": "^4.4.1",
|
|
29
|
+
"@types/node": "^25.0.3",
|
|
30
30
|
"ajv": "^8.17.1",
|
|
31
|
-
"eslint": "^9.
|
|
32
|
-
"eslint-config-prettier": "^
|
|
33
|
-
"eslint-plugin-prettier": "^5.
|
|
31
|
+
"eslint": "^9.39.2",
|
|
32
|
+
"eslint-config-prettier": "^10.1.8",
|
|
33
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
34
34
|
"eslint-plugin-promise": "^7.2.1",
|
|
35
|
-
"prettier": "^3.4
|
|
36
|
-
"ts-json-schema-generator": "^2.
|
|
37
|
-
"tsx": "^4.
|
|
38
|
-
"typescript": "^5.
|
|
39
|
-
"typescript-eslint": "^8.
|
|
35
|
+
"prettier": "^3.7.4",
|
|
36
|
+
"ts-json-schema-generator": "^2.4.0",
|
|
37
|
+
"tsx": "^4.21.0",
|
|
38
|
+
"typescript": "^5.9.3",
|
|
39
|
+
"typescript-eslint": "^8.50.1"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@commander-js/extra-typings": "^
|
|
43
|
-
"commander": "^
|
|
44
|
-
"puppeteer": "^
|
|
42
|
+
"@commander-js/extra-typings": "^14.0.0",
|
|
43
|
+
"commander": "^14.0.2",
|
|
44
|
+
"puppeteer": "^24.34.0"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
|
47
47
|
"node": ">=22.0.0"
|
package/src/cli.ts
CHANGED
|
@@ -3,84 +3,107 @@ import { program } from "@commander-js/extra-typings";
|
|
|
3
3
|
import { MiroBoard } from "./index.js";
|
|
4
4
|
import type { FrameBoardObject } from "./miro-types.ts";
|
|
5
5
|
|
|
6
|
-
const { token, boardId, frameNames, outputFile, exportFormat } =
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
6
|
+
const { token, boardId, frameNames, outputFile, exportFormat, loadTimeout } =
|
|
7
|
+
program
|
|
8
|
+
.option("-t, --token <token>", "Miro token")
|
|
9
|
+
.requiredOption("-b, --board-id <boardId>", "The board ID")
|
|
10
|
+
.option(
|
|
11
|
+
"-f, --frame-names <frameNames...>",
|
|
12
|
+
"The frame name(s), leave empty to export entire board"
|
|
13
|
+
)
|
|
14
|
+
.option(
|
|
15
|
+
"-o, --output-file <filename>",
|
|
16
|
+
"A file to output the SVG to (stdout if not specified)"
|
|
17
|
+
)
|
|
18
|
+
.option("-e, --export-format <format>", "'svg' or 'json'", "svg")
|
|
19
|
+
.option(
|
|
20
|
+
"-l, --load-timeout <milliseconds>",
|
|
21
|
+
"Timeout for loading the board in milliseconds",
|
|
22
|
+
(arg) => parseInt(arg, 10),
|
|
23
|
+
15000
|
|
24
|
+
)
|
|
25
|
+
.parse()
|
|
26
|
+
.opts();
|
|
20
27
|
|
|
21
28
|
(async () => {
|
|
22
|
-
|
|
29
|
+
try {
|
|
30
|
+
await using miroBoard = new MiroBoard({
|
|
31
|
+
token,
|
|
32
|
+
boardId,
|
|
33
|
+
boardLoadTimeoutMs: loadTimeout
|
|
34
|
+
});
|
|
23
35
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
36
|
+
async function getFrames(frameNames: string[]) {
|
|
37
|
+
const frames = await miroBoard.getBoardObjects(
|
|
38
|
+
{ type: "frame" as const },
|
|
39
|
+
{ title: frameNames }
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
if (frames && frames.length !== frameNames.length) {
|
|
43
|
+
throw Error(
|
|
44
|
+
`${
|
|
45
|
+
frameNames.length - frames.length
|
|
46
|
+
} frame(s) could not be found on the board.`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return frames;
|
|
51
|
+
}
|
|
29
52
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
frameNames.length - frames.length
|
|
34
|
-
} frame(s) could not be found on the board.`
|
|
53
|
+
async function getSvg(frames?: FrameBoardObject[]) {
|
|
54
|
+
return await miroBoard.getSvg(
|
|
55
|
+
frames?.map(({ id }) => id).filter((id): id is string => !!id)
|
|
35
56
|
);
|
|
36
57
|
}
|
|
37
58
|
|
|
38
|
-
|
|
39
|
-
|
|
59
|
+
async function getJson(frames?: FrameBoardObject[]) {
|
|
60
|
+
if (frames) {
|
|
61
|
+
const frameChildren = await miroBoard.getBoardObjects({
|
|
62
|
+
id: frames.flatMap((frame) => frame.childrenIds)
|
|
63
|
+
});
|
|
40
64
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
65
|
+
const groupChildren = await miroBoard.getBoardObjects({
|
|
66
|
+
id: frameChildren
|
|
67
|
+
.filter((child) => child.type === "group")
|
|
68
|
+
.flatMap((child) => child.itemsIds)
|
|
69
|
+
});
|
|
46
70
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const frameChildren = await miroBoard.getBoardObjects({
|
|
50
|
-
id: frames.flatMap((frame) => frame.childrenIds)
|
|
51
|
-
});
|
|
71
|
+
return JSON.stringify([...frames, ...frameChildren, ...groupChildren]);
|
|
72
|
+
}
|
|
52
73
|
|
|
53
|
-
|
|
54
|
-
id: frameChildren
|
|
55
|
-
.filter((child) => child.type === "group")
|
|
56
|
-
.flatMap((child) => child.itemsIds)
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
return JSON.stringify([...frames, ...frameChildren, ...groupChildren]);
|
|
74
|
+
return JSON.stringify(await miroBoard.getBoardObjects({}));
|
|
60
75
|
}
|
|
61
76
|
|
|
62
|
-
|
|
63
|
-
}
|
|
77
|
+
const getFn = exportFormat === "json" ? getJson : getSvg;
|
|
64
78
|
|
|
65
|
-
|
|
79
|
+
if (outputFile?.includes("{frameName}")) {
|
|
80
|
+
if (!frameNames) {
|
|
81
|
+
throw Error(
|
|
82
|
+
"Expected frame names to be given when the output file name format expects a frame name."
|
|
83
|
+
);
|
|
84
|
+
}
|
|
66
85
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
for (const frameName of frameNames) {
|
|
75
|
-
const output = await getFn(await getFrames([frameName]));
|
|
76
|
-
await writeFile(outputFile.replace("{frameName}", frameName), output);
|
|
77
|
-
}
|
|
78
|
-
} else {
|
|
79
|
-
const svg = await getFn(frameNames && (await getFrames(frameNames)));
|
|
80
|
-
if (outputFile) {
|
|
81
|
-
await writeFile(outputFile, svg);
|
|
86
|
+
for (const frameName of frameNames) {
|
|
87
|
+
const output = await getFn(await getFrames([frameName]));
|
|
88
|
+
await writeFile(outputFile.replace("{frameName}", frameName), output);
|
|
89
|
+
}
|
|
82
90
|
} else {
|
|
83
|
-
|
|
91
|
+
const svg = await getFn(frameNames && (await getFrames(frameNames)));
|
|
92
|
+
if (outputFile) {
|
|
93
|
+
await writeFile(outputFile, svg);
|
|
94
|
+
} else {
|
|
95
|
+
process.stdout.write(svg);
|
|
96
|
+
}
|
|
84
97
|
}
|
|
98
|
+
} catch (err) {
|
|
99
|
+
const RED = "\x1b[31m";
|
|
100
|
+
const RESET = "\x1b[0m";
|
|
101
|
+
const GRAY = "\x1b[38;5;248m";
|
|
102
|
+
console.error(
|
|
103
|
+
`❌ ${RED}Error:${RESET} ${err instanceof Error ? err.message : err}`
|
|
104
|
+
);
|
|
105
|
+
console.error(GRAY);
|
|
106
|
+
console.error(err);
|
|
107
|
+
console.error(RESET);
|
|
85
108
|
}
|
|
86
109
|
})();
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,32 @@
|
|
|
1
|
-
import puppeteer, { Browser, Page } from "puppeteer";
|
|
1
|
+
import puppeteer, { Browser, Page, type LaunchOptions } from "puppeteer";
|
|
2
2
|
import type { BoardObject } from "./miro-types.ts";
|
|
3
3
|
import type { GetBoardsFilter } from "./miro-runtime.ts";
|
|
4
4
|
|
|
5
5
|
interface InitialMiroBoardOptions {
|
|
6
|
+
/**
|
|
7
|
+
* The Miro authentication token with access to load the board.
|
|
8
|
+
* Optional if anonymous users may access the board without
|
|
9
|
+
* logging in.
|
|
10
|
+
*/
|
|
6
11
|
token?: string;
|
|
12
|
+
/**
|
|
13
|
+
* The Miro board ID.
|
|
14
|
+
*/
|
|
7
15
|
boardId: string;
|
|
16
|
+
/**
|
|
17
|
+
* Optional custom Puppeteer launch options.
|
|
18
|
+
*/
|
|
19
|
+
puppeteerOptions?: LaunchOptions;
|
|
20
|
+
/**
|
|
21
|
+
* Timeout until it is determined that the Miro board could
|
|
22
|
+
* not be loaded for some reason, in milliseconds. Default
|
|
23
|
+
* is 15 seconds (15,000 milliseconds).
|
|
24
|
+
*/
|
|
25
|
+
boardLoadTimeoutMs?: number;
|
|
8
26
|
}
|
|
9
27
|
|
|
28
|
+
const DEFAULT_BOARD_LOAD_TIMEOUT_MS = 15_000;
|
|
29
|
+
|
|
10
30
|
type AdditionalFilter<T> = Partial<T> | Partial<{ [K in keyof T]: T[K][] }>;
|
|
11
31
|
|
|
12
32
|
type FilteredResultsByType<
|
|
@@ -16,13 +36,17 @@ type FilteredResultsByType<
|
|
|
16
36
|
|
|
17
37
|
export class MiroBoard {
|
|
18
38
|
private context = Promise.withResolvers<{ browser: Browser; page: Page }>();
|
|
39
|
+
private isDisposed = false;
|
|
19
40
|
|
|
20
41
|
constructor(options: InitialMiroBoardOptions) {
|
|
21
42
|
this.initialize(options);
|
|
22
43
|
}
|
|
23
44
|
|
|
24
45
|
private async initialize(options: InitialMiroBoardOptions) {
|
|
25
|
-
const browser = await puppeteer.launch({
|
|
46
|
+
const browser = await puppeteer.launch({
|
|
47
|
+
headless: true,
|
|
48
|
+
...(options.puppeteerOptions ?? {})
|
|
49
|
+
});
|
|
26
50
|
const page = await browser.newPage();
|
|
27
51
|
|
|
28
52
|
if (options.token) {
|
|
@@ -44,30 +68,57 @@ export class MiroBoard {
|
|
|
44
68
|
waitUntil: "domcontentloaded"
|
|
45
69
|
});
|
|
46
70
|
|
|
47
|
-
|
|
48
|
-
(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
let miroValue: (typeof window)["miro"];
|
|
55
|
-
Object.defineProperty(window, "miro", {
|
|
56
|
-
get() {
|
|
57
|
-
return miroValue;
|
|
58
|
-
},
|
|
59
|
-
set(value) {
|
|
60
|
-
miroValue = value;
|
|
71
|
+
try {
|
|
72
|
+
await page.evaluate(
|
|
73
|
+
(timeoutDuration) =>
|
|
74
|
+
new Promise<void>((resolve, reject) => {
|
|
75
|
+
if (window.cmd?.board?.api) {
|
|
61
76
|
resolve();
|
|
62
77
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
78
|
+
|
|
79
|
+
const timeout = setTimeout(() => {
|
|
80
|
+
reject(
|
|
81
|
+
new Error(
|
|
82
|
+
`Miro board could not be loaded: application instance not available after ${timeoutDuration} ms. Check your network connection, access token and board access.`
|
|
83
|
+
)
|
|
84
|
+
);
|
|
85
|
+
}, timeoutDuration);
|
|
86
|
+
|
|
87
|
+
const interval = setInterval(() => {
|
|
88
|
+
if (
|
|
89
|
+
document.querySelector('[data-testid="signup-popup-container"]')
|
|
90
|
+
) {
|
|
91
|
+
clearInterval(interval);
|
|
92
|
+
clearTimeout(timeout);
|
|
93
|
+
reject(
|
|
94
|
+
new Error(
|
|
95
|
+
`Miro board requires authentication. Check board access settings to allow anonymous access or supply a token.`
|
|
96
|
+
)
|
|
97
|
+
);
|
|
98
|
+
} else if (window.cmd?.board?.api) {
|
|
99
|
+
clearTimeout(timeout);
|
|
100
|
+
clearInterval(interval);
|
|
101
|
+
resolve();
|
|
102
|
+
}
|
|
103
|
+
}, 250);
|
|
104
|
+
}),
|
|
105
|
+
options.boardLoadTimeoutMs ?? DEFAULT_BOARD_LOAD_TIMEOUT_MS
|
|
106
|
+
);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
await browser.close();
|
|
109
|
+
this.context.reject(err);
|
|
110
|
+
this.isDisposed = true;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
66
113
|
|
|
67
114
|
this.context.resolve({ browser, page });
|
|
68
115
|
}
|
|
69
116
|
|
|
70
117
|
async dispose() {
|
|
118
|
+
if (this.isDisposed) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
this.isDisposed = true;
|
|
71
122
|
await (await this.browser).close();
|
|
72
123
|
}
|
|
73
124
|
|
|
@@ -83,10 +134,52 @@ export class MiroBoard {
|
|
|
83
134
|
return this.context.promise.then(({ page }) => page);
|
|
84
135
|
}
|
|
85
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Checks that all widgets have been loaded. Times out to 5 seconds due to bug
|
|
139
|
+
* in Miro where certain boards fail to load all widgets if SDK is not required
|
|
140
|
+
* for this function call.
|
|
141
|
+
*/
|
|
142
|
+
private async checkAllWidgetsLoaded(mustHaveSdk: boolean = false) {
|
|
143
|
+
await (
|
|
144
|
+
await this.page
|
|
145
|
+
).evaluate(
|
|
146
|
+
(mustHaveSdk, timeoutDuration) =>
|
|
147
|
+
new Promise<void>((resolve, reject) => {
|
|
148
|
+
const startTime = Date.now();
|
|
149
|
+
const interval = setInterval(() => {
|
|
150
|
+
if (
|
|
151
|
+
window.cmd?.board?.api?.isAllWidgetsLoaded() &&
|
|
152
|
+
(!mustHaveSdk || window.miro)
|
|
153
|
+
) {
|
|
154
|
+
clearInterval(interval);
|
|
155
|
+
resolve();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (Date.now() - startTime >= timeoutDuration) {
|
|
159
|
+
clearInterval(interval);
|
|
160
|
+
if (mustHaveSdk) {
|
|
161
|
+
reject(
|
|
162
|
+
new Error(
|
|
163
|
+
`Miro SDK failed to load in ${timeoutDuration} ms. This is likely caused by a Miro-internal issue. Check that the board is accessible using an incognito browser window.`
|
|
164
|
+
)
|
|
165
|
+
);
|
|
166
|
+
} else {
|
|
167
|
+
resolve();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}, 150);
|
|
171
|
+
}),
|
|
172
|
+
mustHaveSdk,
|
|
173
|
+
3_000
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
86
177
|
async getBoardObjects<F extends GetBoardsFilter>(
|
|
87
178
|
filter: F,
|
|
88
179
|
additionalFilter?: AdditionalFilter<BoardObject>
|
|
89
180
|
): Promise<FilteredResultsByType<F["type"], BoardObject>> {
|
|
181
|
+
await this.checkAllWidgetsLoaded(true);
|
|
182
|
+
|
|
90
183
|
return (await this.page).evaluate(
|
|
91
184
|
async (filter, additionalFilter) => {
|
|
92
185
|
// @ts-expect-error - https://github.com/evanw/esbuild/issues/2605#issuecomment-2050808084
|
|
@@ -130,8 +223,10 @@ export class MiroBoard {
|
|
|
130
223
|
}
|
|
131
224
|
|
|
132
225
|
async getSvg(objectsIds?: string[]) {
|
|
226
|
+
await this.checkAllWidgetsLoaded(objectsIds !== undefined);
|
|
227
|
+
|
|
133
228
|
return (await this.page).evaluate(async (objectsIds) => {
|
|
134
|
-
|
|
229
|
+
window.cmd.board.api.clearSelection();
|
|
135
230
|
|
|
136
231
|
if (objectsIds) {
|
|
137
232
|
for (const id of objectsIds) {
|
package/src/miro-runtime.d.ts
CHANGED
package/src/miro-types.ts
CHANGED
|
@@ -69,15 +69,14 @@ interface BoardObjectWithDimensions {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
export interface PreviewBoardObject
|
|
72
|
-
extends BoardItemBase,
|
|
73
|
-
BoardObjectBase,
|
|
74
|
-
BoardObjectWithDimensions {
|
|
72
|
+
extends BoardItemBase, BoardObjectBase, BoardObjectWithDimensions {
|
|
75
73
|
type: "preview";
|
|
76
74
|
url: string;
|
|
77
75
|
}
|
|
78
76
|
|
|
79
77
|
export interface FrameBoardObject
|
|
80
|
-
extends
|
|
78
|
+
extends
|
|
79
|
+
Omit<BoardItemBase, "connectorIds">,
|
|
81
80
|
BoardObjectBase,
|
|
82
81
|
BoardObjectWithDimensions,
|
|
83
82
|
BoardObjectWithCoordinates {
|
|
@@ -96,7 +95,8 @@ export interface GroupBoardItem extends BoardItemBase {
|
|
|
96
95
|
}
|
|
97
96
|
|
|
98
97
|
export interface StickyNoteBoardObject
|
|
99
|
-
extends
|
|
98
|
+
extends
|
|
99
|
+
BoardItemBase,
|
|
100
100
|
BoardObjectBase,
|
|
101
101
|
BoardObjectWithCoordinates,
|
|
102
102
|
BoardObjectWithDimensions {
|
|
@@ -112,7 +112,8 @@ export interface StickyNoteBoardObject
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
export interface TextBoardObject
|
|
115
|
-
extends
|
|
115
|
+
extends
|
|
116
|
+
BoardItemBase,
|
|
116
117
|
BoardObjectBase,
|
|
117
118
|
BoardObjectWithCoordinates,
|
|
118
119
|
BoardObjectWithDimensions {
|
|
@@ -130,7 +131,8 @@ export interface TextBoardObject
|
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
export interface ImageBoardObject
|
|
133
|
-
extends
|
|
134
|
+
extends
|
|
135
|
+
BoardItemBase,
|
|
134
136
|
BoardObjectBase,
|
|
135
137
|
BoardObjectWithDimensions,
|
|
136
138
|
BoardObjectWithCoordinates {
|
|
@@ -159,9 +161,7 @@ interface TableStyle {
|
|
|
159
161
|
}
|
|
160
162
|
|
|
161
163
|
export interface TableBoardObject
|
|
162
|
-
extends BoardItemBase,
|
|
163
|
-
BoardObjectWithDimensions,
|
|
164
|
-
BoardObjectWithCoordinates {
|
|
164
|
+
extends BoardItemBase, BoardObjectWithDimensions, BoardObjectWithCoordinates {
|
|
165
165
|
type: "image";
|
|
166
166
|
rotation: number;
|
|
167
167
|
cols?: number | { width: number }[];
|
|
@@ -176,7 +176,8 @@ export interface TableBoardObject
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
export interface StructDocBoardObject
|
|
179
|
-
extends
|
|
179
|
+
extends
|
|
180
|
+
BoardItemBase,
|
|
180
181
|
BoardObjectBase,
|
|
181
182
|
BoardObjectWithDimensions,
|
|
182
183
|
BoardObjectWithCoordinates {
|
|
@@ -186,9 +187,7 @@ export interface StructDocBoardObject
|
|
|
186
187
|
}
|
|
187
188
|
|
|
188
189
|
export interface EmbedBoardObject
|
|
189
|
-
extends BoardItemBase,
|
|
190
|
-
BoardObjectBase,
|
|
191
|
-
BoardObjectWithCoordinates {
|
|
190
|
+
extends BoardItemBase, BoardObjectBase, BoardObjectWithCoordinates {
|
|
192
191
|
type: "struct_doc";
|
|
193
192
|
url: string;
|
|
194
193
|
previewUrl: string;
|
|
@@ -198,7 +197,8 @@ export interface EmbedBoardObject
|
|
|
198
197
|
}
|
|
199
198
|
|
|
200
199
|
export interface ShapeBoardObject
|
|
201
|
-
extends
|
|
200
|
+
extends
|
|
201
|
+
BoardItemBase,
|
|
202
202
|
BoardObjectBase,
|
|
203
203
|
BoardObjectWithCoordinates,
|
|
204
204
|
BoardObjectWithDimensions {
|
|
@@ -265,7 +265,8 @@ export interface TagBoardItem extends BoardItemBase {
|
|
|
265
265
|
}
|
|
266
266
|
|
|
267
267
|
export interface KanbanBoardObject
|
|
268
|
-
extends
|
|
268
|
+
extends
|
|
269
|
+
BoardItemBase,
|
|
269
270
|
BoardObjectBase,
|
|
270
271
|
BoardObjectWithCoordinates,
|
|
271
272
|
BoardObjectWithDimensions {
|
|
@@ -273,7 +274,8 @@ export interface KanbanBoardObject
|
|
|
273
274
|
}
|
|
274
275
|
|
|
275
276
|
export interface UnsupportedBoardObject
|
|
276
|
-
extends
|
|
277
|
+
extends
|
|
278
|
+
BoardItemBase,
|
|
277
279
|
BoardObjectBase,
|
|
278
280
|
BoardObjectWithCoordinates,
|
|
279
281
|
BoardObjectWithDimensions {
|
package/tests/api.test.ts
CHANGED
|
@@ -3,12 +3,24 @@ import assert from "node:assert";
|
|
|
3
3
|
import { MiroBoard } from "../src";
|
|
4
4
|
|
|
5
5
|
const boardId = process.env.TEST_BOARD_ID;
|
|
6
|
+
const inaccessibleBoardId = process.env.PRIVATE_TEST_BOARD_ID;
|
|
7
|
+
const buggyBoardId = process.env.BUGGY_TEST_BOARD_ID;
|
|
6
8
|
|
|
7
9
|
if (!boardId) {
|
|
8
10
|
console.error("TEST_BOARD_ID environment variable is required.");
|
|
9
11
|
process.exit(1);
|
|
10
12
|
}
|
|
11
13
|
|
|
14
|
+
if (!inaccessibleBoardId) {
|
|
15
|
+
console.error("PRIVATE_TEST_BOARD_ID environment variable is required.");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!buggyBoardId) {
|
|
20
|
+
console.error("BUGGY_TEST_BOARD_ID environment variable is required.");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
12
24
|
describe("Miro integration", async () => {
|
|
13
25
|
const miroBoard = new MiroBoard({ boardId });
|
|
14
26
|
|
|
@@ -66,3 +78,32 @@ describe("Miro integration", async () => {
|
|
|
66
78
|
assert.ok(svg.length > 10_000);
|
|
67
79
|
});
|
|
68
80
|
});
|
|
81
|
+
|
|
82
|
+
await it("should throw error for a non-public board", async () => {
|
|
83
|
+
try {
|
|
84
|
+
const miroBoard = new MiroBoard({ boardId: inaccessibleBoardId });
|
|
85
|
+
await miroBoard.getBoardObjects({});
|
|
86
|
+
assert.fail();
|
|
87
|
+
} catch (err) {
|
|
88
|
+
assert.ok(err instanceof Error);
|
|
89
|
+
assert.equal(
|
|
90
|
+
err.message,
|
|
91
|
+
"Miro board requires authentication. Check board access settings to allow anonymous access or supply a token."
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await it("should be able to export SVG of a buggy Miro board", async () => {
|
|
97
|
+
// this is a flaky issue on Miro; around 10% of the time the board loads fine
|
|
98
|
+
// so let's repeat this test a couple of times just to be sure
|
|
99
|
+
const results = await Promise.all(
|
|
100
|
+
Array.from({ length: 10 }).map(async () => {
|
|
101
|
+
const miroBoard = new MiroBoard({ boardId: buggyBoardId });
|
|
102
|
+
const svg = await miroBoard.getSvg();
|
|
103
|
+
const hasSvgContents = svg.includes("What should we do next?");
|
|
104
|
+
await miroBoard.dispose();
|
|
105
|
+
return hasSvgContents;
|
|
106
|
+
})
|
|
107
|
+
);
|
|
108
|
+
assert.deepStrictEqual(results, Array.from({ length: 10 }).fill(true));
|
|
109
|
+
});
|