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.
@@ -9,7 +9,7 @@ jobs:
9
9
  runs-on: ubuntu-latest
10
10
  steps:
11
11
  - uses: actions/checkout@v6
12
- - uses: pnpm/action-setup@v3
12
+ - uses: pnpm/action-setup@v4
13
13
  with:
14
14
  version: 8
15
15
  - uses: actions/setup-node@v6
@@ -9,7 +9,7 @@ jobs:
9
9
  runs-on: ubuntu-latest
10
10
  steps:
11
11
  - uses: actions/checkout@v6
12
- - uses: pnpm/action-setup@v3
12
+ - uses: pnpm/action-setup@v4
13
13
  with:
14
14
  version: 8
15
15
  - uses: actions/setup-node@v6
@@ -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@v3
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 choise in every case. Alternatively,
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.miro) {
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 authModalChecker = setInterval(() => {
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
- }, 250);
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(authModalChecker);
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
- await window.miro.board.deselect();
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
- return await window.cmd.board.api.export.makeVector();
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miro-export",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "author": "jolle <npm-contact@jolle.io>",
5
5
  "license": "MIT",
6
6
  "type": "module",
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.miro) {
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 authModalChecker = setInterval(() => {
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(authModalChecker);
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
- await window.miro.board.deselect();
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
- return await window.cmd.board.api.export.makeVector();
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
  }
@@ -21,6 +21,8 @@ declare global {
21
21
  export: {
22
22
  makeVector: () => Promise<string>;
23
23
  };
24
+ clearSelection: () => void;
25
+ isAllWidgetsLoaded: () => boolean;
24
26
  };
25
27
  };
26
28
  };
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
+ });