miro-export 1.2.0 → 1.3.1
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 +1 -1
- package/.github/workflows/publish.yml +1 -1
- package/.github/workflows/test.yml +2 -1
- package/README.md +1 -1
- package/build/index.d.ts +6 -0
- package/build/index.js +47 -14
- package/package.json +1 -1
- package/src/index.ts +63 -16
- package/src/miro-runtime.d.ts +2 -0
- package/tests/api.test.ts +24 -0
|
@@ -9,7 +9,7 @@ jobs:
|
|
|
9
9
|
runs-on: ubuntu-22.04
|
|
10
10
|
steps:
|
|
11
11
|
- uses: actions/checkout@v6
|
|
12
|
-
- uses: pnpm/action-setup@
|
|
12
|
+
- uses: pnpm/action-setup@v4
|
|
13
13
|
with:
|
|
14
14
|
version: 8
|
|
15
15
|
- uses: actions/setup-node@v6
|
|
@@ -22,3 +22,4 @@ jobs:
|
|
|
22
22
|
env:
|
|
23
23
|
TEST_BOARD_ID: ${{secrets.TEST_BOARD_ID}}
|
|
24
24
|
PRIVATE_TEST_BOARD_ID: ${{secrets.PRIVATE_TEST_BOARD_ID}}
|
|
25
|
+
BUGGY_TEST_BOARD_ID: ${{secrets.BUGGY_TEST_BOARD_ID}}
|
package/README.md
CHANGED
|
@@ -117,7 +117,7 @@ const svgOfFrame1 = await miroBoard.getSvg([framesWithTitleFrame1[0].id]);
|
|
|
117
117
|
> [!WARNING]
|
|
118
118
|
> Remember to dispose the instance to make sure the browser is closed and the process
|
|
119
119
|
> can exit. `await using` (as shown above) does this automatically, but is not supported
|
|
120
|
-
> in all environments and may not be the optimal
|
|
120
|
+
> in all environments and may not be the optimal choice in every case. Alternatively,
|
|
121
121
|
> `miroBoard.dispose()` may be called at any time to dispose of the instance manually.
|
|
122
122
|
|
|
123
123
|
Types for many of the common board object types has been provided in [miro-types.ts](src/miro-types.ts).
|
package/build/index.d.ts
CHANGED
|
@@ -38,6 +38,12 @@ export declare class MiroBoard {
|
|
|
38
38
|
[Symbol.asyncDispose](): Promise<void>;
|
|
39
39
|
private get browser();
|
|
40
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;
|
|
41
47
|
getBoardObjects<F extends GetBoardsFilter>(filter: F, additionalFilter?: AdditionalFilter<BoardObject>): Promise<FilteredResultsByType<F["type"], BoardObject>>;
|
|
42
48
|
getSvg(objectsIds?: string[]): Promise<string>;
|
|
43
49
|
}
|
package/build/index.js
CHANGED
|
@@ -30,29 +30,24 @@ export class MiroBoard {
|
|
|
30
30
|
});
|
|
31
31
|
try {
|
|
32
32
|
await page.evaluate((timeoutDuration) => new Promise((resolve, reject) => {
|
|
33
|
-
if (window.
|
|
33
|
+
if (window.cmd?.board?.api) {
|
|
34
34
|
resolve();
|
|
35
35
|
}
|
|
36
36
|
const timeout = setTimeout(() => {
|
|
37
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
38
|
}, timeoutDuration);
|
|
39
|
-
const
|
|
39
|
+
const interval = setInterval(() => {
|
|
40
40
|
if (document.querySelector('[data-testid="signup-popup-container"]')) {
|
|
41
|
+
clearInterval(interval);
|
|
42
|
+
clearTimeout(timeout);
|
|
41
43
|
reject(new Error(`Miro board requires authentication. Check board access settings to allow anonymous access or supply a token.`));
|
|
42
44
|
}
|
|
43
|
-
|
|
44
|
-
let miroValue;
|
|
45
|
-
Object.defineProperty(window, "miro", {
|
|
46
|
-
get() {
|
|
47
|
-
return miroValue;
|
|
48
|
-
},
|
|
49
|
-
set(value) {
|
|
45
|
+
else if (window.cmd?.board?.api) {
|
|
50
46
|
clearTimeout(timeout);
|
|
51
|
-
clearInterval(
|
|
52
|
-
miroValue = value;
|
|
47
|
+
clearInterval(interval);
|
|
53
48
|
resolve();
|
|
54
49
|
}
|
|
55
|
-
});
|
|
50
|
+
}, 250);
|
|
56
51
|
}), options.boardLoadTimeoutMs ?? DEFAULT_BOARD_LOAD_TIMEOUT_MS);
|
|
57
52
|
}
|
|
58
53
|
catch (err) {
|
|
@@ -79,7 +74,34 @@ export class MiroBoard {
|
|
|
79
74
|
get page() {
|
|
80
75
|
return this.context.promise.then(({ page }) => page);
|
|
81
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
|
+
}
|
|
82
103
|
async getBoardObjects(filter, additionalFilter) {
|
|
104
|
+
await this.checkAllWidgetsLoaded(true);
|
|
83
105
|
return (await this.page).evaluate(async (filter, additionalFilter) => {
|
|
84
106
|
// @ts-expect-error - https://github.com/evanw/esbuild/issues/2605#issuecomment-2050808084
|
|
85
107
|
window.__name = (func) => func;
|
|
@@ -108,14 +130,25 @@ export class MiroBoard {
|
|
|
108
130
|
}, filter, additionalFilter);
|
|
109
131
|
}
|
|
110
132
|
async getSvg(objectsIds) {
|
|
133
|
+
await this.checkAllWidgetsLoaded(objectsIds !== undefined);
|
|
111
134
|
return (await this.page).evaluate(async (objectsIds) => {
|
|
112
|
-
|
|
135
|
+
window.cmd.board.api.clearSelection();
|
|
113
136
|
if (objectsIds) {
|
|
114
137
|
for (const id of objectsIds) {
|
|
115
138
|
await window.miro.board.select({ id });
|
|
116
139
|
}
|
|
117
140
|
}
|
|
118
|
-
|
|
141
|
+
let error;
|
|
142
|
+
for (let retry = 0; retry < 5; retry++) {
|
|
143
|
+
try {
|
|
144
|
+
return await window.cmd.board.api.export.makeVector();
|
|
145
|
+
}
|
|
146
|
+
catch (e) {
|
|
147
|
+
error = e;
|
|
148
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
throw error ?? Error("Failed to export vector");
|
|
119
152
|
}, objectsIds);
|
|
120
153
|
}
|
|
121
154
|
}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -72,7 +72,7 @@ export class MiroBoard {
|
|
|
72
72
|
await page.evaluate(
|
|
73
73
|
(timeoutDuration) =>
|
|
74
74
|
new Promise<void>((resolve, reject) => {
|
|
75
|
-
if (window.
|
|
75
|
+
if (window.cmd?.board?.api) {
|
|
76
76
|
resolve();
|
|
77
77
|
}
|
|
78
78
|
|
|
@@ -84,30 +84,23 @@ export class MiroBoard {
|
|
|
84
84
|
);
|
|
85
85
|
}, timeoutDuration);
|
|
86
86
|
|
|
87
|
-
const
|
|
87
|
+
const interval = setInterval(() => {
|
|
88
88
|
if (
|
|
89
89
|
document.querySelector('[data-testid="signup-popup-container"]')
|
|
90
90
|
) {
|
|
91
|
+
clearInterval(interval);
|
|
92
|
+
clearTimeout(timeout);
|
|
91
93
|
reject(
|
|
92
94
|
new Error(
|
|
93
95
|
`Miro board requires authentication. Check board access settings to allow anonymous access or supply a token.`
|
|
94
96
|
)
|
|
95
97
|
);
|
|
96
|
-
}
|
|
97
|
-
}, 250);
|
|
98
|
-
|
|
99
|
-
let miroValue: (typeof window)["miro"];
|
|
100
|
-
Object.defineProperty(window, "miro", {
|
|
101
|
-
get() {
|
|
102
|
-
return miroValue;
|
|
103
|
-
},
|
|
104
|
-
set(value) {
|
|
98
|
+
} else if (window.cmd?.board?.api) {
|
|
105
99
|
clearTimeout(timeout);
|
|
106
|
-
clearInterval(
|
|
107
|
-
miroValue = value;
|
|
100
|
+
clearInterval(interval);
|
|
108
101
|
resolve();
|
|
109
102
|
}
|
|
110
|
-
});
|
|
103
|
+
}, 250);
|
|
111
104
|
}),
|
|
112
105
|
options.boardLoadTimeoutMs ?? DEFAULT_BOARD_LOAD_TIMEOUT_MS
|
|
113
106
|
);
|
|
@@ -141,10 +134,52 @@ export class MiroBoard {
|
|
|
141
134
|
return this.context.promise.then(({ page }) => page);
|
|
142
135
|
}
|
|
143
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
|
+
|
|
144
177
|
async getBoardObjects<F extends GetBoardsFilter>(
|
|
145
178
|
filter: F,
|
|
146
179
|
additionalFilter?: AdditionalFilter<BoardObject>
|
|
147
180
|
): Promise<FilteredResultsByType<F["type"], BoardObject>> {
|
|
181
|
+
await this.checkAllWidgetsLoaded(true);
|
|
182
|
+
|
|
148
183
|
return (await this.page).evaluate(
|
|
149
184
|
async (filter, additionalFilter) => {
|
|
150
185
|
// @ts-expect-error - https://github.com/evanw/esbuild/issues/2605#issuecomment-2050808084
|
|
@@ -188,8 +223,10 @@ export class MiroBoard {
|
|
|
188
223
|
}
|
|
189
224
|
|
|
190
225
|
async getSvg(objectsIds?: string[]) {
|
|
226
|
+
await this.checkAllWidgetsLoaded(objectsIds !== undefined);
|
|
227
|
+
|
|
191
228
|
return (await this.page).evaluate(async (objectsIds) => {
|
|
192
|
-
|
|
229
|
+
window.cmd.board.api.clearSelection();
|
|
193
230
|
|
|
194
231
|
if (objectsIds) {
|
|
195
232
|
for (const id of objectsIds) {
|
|
@@ -197,7 +234,17 @@ export class MiroBoard {
|
|
|
197
234
|
}
|
|
198
235
|
}
|
|
199
236
|
|
|
200
|
-
|
|
237
|
+
let error: unknown;
|
|
238
|
+
for (let retry = 0; retry < 5; retry++) {
|
|
239
|
+
try {
|
|
240
|
+
return await window.cmd.board.api.export.makeVector();
|
|
241
|
+
} catch (e) {
|
|
242
|
+
error = e;
|
|
243
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
throw error ?? Error("Failed to export vector");
|
|
201
248
|
}, objectsIds);
|
|
202
249
|
}
|
|
203
250
|
}
|
package/src/miro-runtime.d.ts
CHANGED
package/tests/api.test.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { MiroBoard } from "../src";
|
|
|
4
4
|
|
|
5
5
|
const boardId = process.env.TEST_BOARD_ID;
|
|
6
6
|
const inaccessibleBoardId = process.env.PRIVATE_TEST_BOARD_ID;
|
|
7
|
+
const buggyBoardId = process.env.BUGGY_TEST_BOARD_ID;
|
|
7
8
|
|
|
8
9
|
if (!boardId) {
|
|
9
10
|
console.error("TEST_BOARD_ID environment variable is required.");
|
|
@@ -15,6 +16,11 @@ if (!inaccessibleBoardId) {
|
|
|
15
16
|
process.exit(1);
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
if (!buggyBoardId) {
|
|
20
|
+
console.error("BUGGY_TEST_BOARD_ID environment variable is required.");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
describe("Miro integration", async () => {
|
|
19
25
|
const miroBoard = new MiroBoard({ boardId });
|
|
20
26
|
|
|
@@ -86,3 +92,21 @@ await it("should throw error for a non-public board", async () => {
|
|
|
86
92
|
);
|
|
87
93
|
}
|
|
88
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
|
+
for (let i = 0; i < 3; i++) {
|
|
100
|
+
const miroBoard = new MiroBoard({
|
|
101
|
+
boardId: buggyBoardId,
|
|
102
|
+
boardLoadTimeoutMs: 30_000
|
|
103
|
+
});
|
|
104
|
+
try {
|
|
105
|
+
const svg = await miroBoard.getSvg();
|
|
106
|
+
const hasSvgContents = svg.includes("What should we do next?");
|
|
107
|
+
assert.ok(hasSvgContents);
|
|
108
|
+
} finally {
|
|
109
|
+
await miroBoard.dispose();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
});
|