figma-rust-mcp 3.2.15 → 4.0.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/README.md +58 -23
- package/package.json +1 -1
- package/scripts/build-plugin.js +1 -0
- package/scripts/test-bridge-sessions.mjs +8 -3
- package/scripts/test-plugin-tokens.mjs +91 -13
package/README.md
CHANGED
|
@@ -10,9 +10,9 @@ Enables AI agents (Google Antigravity, Claude Code, Cursor, Windsurf, VS Code, Z
|
|
|
10
10
|
|
|
11
11
|
- **Lossless Rust Tree Compression**: Compact/minimal tree responses share identical style bundles when the complete JSON becomes smaller. Node IDs, hierarchy, text, geometry and state differences remain intact. Full detail bypasses Rust compression; no heuristic merging of screens or repeated items.
|
|
12
12
|
- **Pure Rust Native Performance**: Starts in `< 1ms`, uses `~3MB RAM`, zero GC pauses.
|
|
13
|
-
- **In-Memory
|
|
13
|
+
- **Scoped In-Memory Index (`figma_index`)**: Indexes page roots first and loads frame descendants on demand. Search reports the indexed scope and completeness.
|
|
14
14
|
- **Binary IPC & Chunk Streaming**: Powered by **MessagePack** (`rmp-serde`) and progressive subtree chunking for instant transfers of massive design files.
|
|
15
|
-
- **
|
|
15
|
+
- **Revisioned Updates**: Property patches update affected nodes; revision gaps trigger a fresh snapshot.
|
|
16
16
|
- **Instant Design-to-Code (`figma_to_code`)**: Compiles Figma frames into clean, semantic components (**React + Tailwind**, **React Native**, **Vue 3 + Tailwind**, **HTML**, **SwiftUI**).
|
|
17
17
|
- **Design Token Exporter (`figma_get_tokens`)**: Exports variables and styles to **Tailwind config**, **CSS custom properties** (`:root` & dark mode), **TypeScript consts**, or **W3C DTCG Token Studio JSON**.
|
|
18
18
|
- **Batch Asset Extractor (`figma_export_assets` / `figma_export_asset`)**: Extracts SVG icons and raster images directly to project folders (`outputPath`) with auto-generated TypeScript barrel exports (`index.ts`) — zero chat token bloating.
|
|
@@ -217,7 +217,7 @@ Figma plugin only if the reported hash does not change.
|
|
|
217
217
|
Checks live bridge connection status, connected Figma tabs/files, in-memory index health, queue length, and latency statistics.
|
|
218
218
|
|
|
219
219
|
### 2. `figma_inspect_node`
|
|
220
|
-
|
|
220
|
+
Reads a specific node by ID or name through the canonical flat `read_nodes` response. Select fields explicitly to control the work performed.
|
|
221
221
|
|
|
222
222
|
### 3. `figma_to_code`
|
|
223
223
|
Compiles any Figma node (Frame, Component, Section, or selection) directly into clean, production-ready component code.
|
|
@@ -291,7 +291,7 @@ Instant `< 1ms` in-memory queries against pre-indexed Figma file structures.
|
|
|
291
291
|
- `"search_components"`: Find component sets and variants.
|
|
292
292
|
- `"search_styles"`: Find paint, text, and effect styles.
|
|
293
293
|
- `"search_variables"`: Find design token variables by name or collection.
|
|
294
|
-
- `"refresh"`:
|
|
294
|
+
- `"refresh"`: Refresh page roots, or expand the frame specified by `nodeId`.
|
|
295
295
|
- `"subtree"`: Read/cache one `nodeId`, default depth 2 and 1000 nodes. Increase `depth` or `maxNodes` only for the section you need; check `meta` for truncation.
|
|
296
296
|
- `"typography"`: Rust-generated text/font table for one `nodeId`, with mixed-style runs preserved. Default limit 200 rows. Null font fields mean unknown/mixed, not a guessed default.
|
|
297
297
|
|
|
@@ -301,8 +301,8 @@ For typography verification, call `figma_verify_ui` with a text `nodeId` and bro
|
|
|
301
301
|
|
|
302
302
|
### 9. `figma_read`
|
|
303
303
|
Universal reader for advanced queries:
|
|
304
|
-
- **Design-to-code**: `
|
|
305
|
-
- **Inspection & Hierarchy**: `
|
|
304
|
+
- **Design-to-code**: `read_nodes`, `get_css`, `get_component_map`, `get_unmapped_components`.
|
|
305
|
+
- **Inspection & Hierarchy**: `read_nodes`, `get_selection`, `get_page_nodes`, `scan_design`, `search_nodes`.
|
|
306
306
|
- **Design Systems**: `get_styles`, `get_variables`, `get_local_components`, `get_tokens`.
|
|
307
307
|
- **Visuals & Canvas**: `screenshot`, `export_svg`, `export_image`, `get_viewport`.
|
|
308
308
|
|
|
@@ -366,23 +366,58 @@ Tools on one tab run sequentially, including every awaited operation in a
|
|
|
366
366
|
(Node >=22) checks real WebSocket/HTTP transport with a simulated plugin canvas;
|
|
367
367
|
it does not replace validation in Figma Desktop.
|
|
368
368
|
|
|
369
|
-
### Startup indexing
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
369
|
+
### Startup indexing and v4 migration
|
|
370
|
+
|
|
371
|
+
Version 4 requires both server and plugin runtime protocol 3. Update both and
|
|
372
|
+
restart the plugin. Opening the bridge indexes only visible top-level nodes on
|
|
373
|
+
the active page, plus local styles and variables. Selection expands the selected
|
|
374
|
+
frame. Component catalogue discovery is deferred; request it with
|
|
375
|
+
`figma_index({ operation: "refresh", includeComponents: true })`.
|
|
376
|
+
|
|
377
|
+
Search covers indexed scopes only. Inspect `scope` and `complete` in search
|
|
378
|
+
responses; an empty result does not prove a layer is absent from the file.
|
|
379
|
+
Refresh a specific frame to expand its scope:
|
|
380
|
+
|
|
381
|
+
```js
|
|
382
|
+
figma_index({ operation: "refresh", nodeId: "123:456", expandInstances: true })
|
|
383
|
+
figma_read({ operation: "read_nodes", nodeId: "123:456",
|
|
384
|
+
fields: ["geometry", "content", "text", "style", "layout", "tokens", "component"],
|
|
385
|
+
limit: 200, expandInstances: true })
|
|
386
|
+
// Continue using the returned cursor; do not repeat or change traversal options.
|
|
387
|
+
figma_read({ operation: "read_nodes", cursor: nextCursor, limit: 200 })
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
`read_nodes` returns a flat envelope:
|
|
391
|
+
`{ schemaVersion: 4, pageId, revision, scope, nodes, nextCursor, complete, totalRead, budgetReached }`.
|
|
392
|
+
Identity and topology are always included. Default fields are geometry and
|
|
393
|
+
content; typography, paints, layout, variable/style references and component
|
|
394
|
+
metadata are opt-in. Token fields contain references, not resolved CSS values.
|
|
395
|
+
Page reads default to depth 0; frame reads default to full traversal (maximum
|
|
396
|
+
256 levels). Limit is 1?500 nodes per response; traversal stops at 50,000 visited
|
|
397
|
+
nodes. `complete` describes the requested scope, including its depth and filters.
|
|
398
|
+
Restart from the root if the page or document revision changes between cursor
|
|
399
|
+
requests. Cursors are single-use and at most 16 traversals can remain open.
|
|
400
|
+
|
|
401
|
+
Instances are opaque by default: `opaque: true` and no descendant traversal.
|
|
402
|
+
Set `expandInstances: true` to read the actual instance children and overrides.
|
|
403
|
+
Hidden nodes require `includeHidden: true`. Expansion and visibility are separate
|
|
404
|
+
options; no master component is substituted for an instance.
|
|
405
|
+
|
|
406
|
+
Breaking output changes: public `get_design`, `get_design_context` and
|
|
407
|
+
`get_node_detail` aliases now return the same envelope as `read_nodes`.
|
|
408
|
+
`figma_inspect_node` and index `get_node`, `subtree`, `typography` also return
|
|
409
|
+
that envelope. Replace nested `children` consumers with `nodes` and parent/child
|
|
410
|
+
IDs. Replace index `maxNodes` with `limit` and cursor pagination. Internal
|
|
411
|
+
script helpers used for code generation retain their specialized formats.
|
|
412
|
+
|
|
413
|
+
Canvas traversal reads each child list once, yields after an 8ms slice, and
|
|
414
|
+
streams chunks of up to 500 nodes. Snapshots commit atomically; frame refreshes
|
|
415
|
+
preserve other indexed frames. Revisioned create/delete/property patches
|
|
416
|
+
invalidate affected cached reads, while unrelated nodes remain queryable.
|
|
417
|
+
Structural additions mark coverage incomplete until the scope is reread.
|
|
418
|
+
Revision gaps request resynchronization. Page switches rebuild the shallow
|
|
419
|
+
index without preloading every page. Exact-read caching evicts one least
|
|
420
|
+
recently used entry when its 64-entry capacity is reached.
|
|
386
421
|
|
|
387
422
|
## 💻 Development & Testing
|
|
388
423
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "figma-rust-mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Claude Code to Figma — Bidirectional MCP bridge. AI draws UI on Figma canvas, reads designs back. Works with Claude, Cursor, VS Code, Windsurf, Antigravity. No API key needed.",
|
|
5
5
|
"mcpName": "io.github.BuiHung1612/figma-rust-mcp",
|
|
6
6
|
"type": "module",
|
package/scripts/build-plugin.js
CHANGED
|
@@ -64,6 +64,7 @@ test('multiple tabs, reconnect ownership and independent frame tasks across MCP
|
|
|
64
64
|
state.runtime = vm.createContext({ handlers: {}, findNodeByIdAsync: async id => nodes.get(id), yieldToUI: async () => {} });
|
|
65
65
|
vm.runInContext(readFileSync('plugin-src/task-scope.js', 'utf8'), state.runtime);
|
|
66
66
|
state.runtime.handlers.get_design = async params => ({ id: params.id, type: 'FRAME', name: params.id, tab: sid });
|
|
67
|
+
state.runtime.handlers.read_nodes = async params => ({ schemaVersion: 4, pageId: 'page', revision: 0, scope: { id: params.id }, nodes: [{ id: params.id, type: 'FRAME', name: params.id, tab: sid }], nextCursor: null, complete: true });
|
|
67
68
|
state.runtime.handlers.status = async () => ({ tab: sid });
|
|
68
69
|
state.runtime.handlers.modify = async params => {
|
|
69
70
|
actions.push(params.note);
|
|
@@ -92,11 +93,15 @@ test('multiple tabs, reconnect ownership and independent frame tasks across MCP
|
|
|
92
93
|
});
|
|
93
94
|
});
|
|
94
95
|
await new Promise((resolve, reject) => { socket.addEventListener('open', resolve, { once: true }); socket.addEventListener('error', reject, { once: true }); });
|
|
95
|
-
socket.send(JSON.stringify({ type: 'runtime-capabilities', runtimeVersion: 'test', protocolVersion:
|
|
96
|
+
socket.send(JSON.stringify({ type: 'runtime-capabilities', runtimeVersion: 'test', protocolVersion: 3, documentId: 'same-document', operations: Object.keys(state.runtime.handlers) }));
|
|
96
97
|
return state;
|
|
97
98
|
}
|
|
98
99
|
const tabA = await plugin('tab-a'), tabB = await plugin('tab-b');
|
|
99
100
|
const first = agent(), second = agent();
|
|
101
|
+
const scopedSearch = decode(ok(await rpc('figma_read', { operation: 'search_nodes', query: 'child', sessionId: 'tab-a' })));
|
|
102
|
+
assert.equal(scopedSearch.complete, false);
|
|
103
|
+
assert.deepEqual(scopedSearch.nodes, []);
|
|
104
|
+
|
|
100
105
|
const ambiguous = await rpc('figma_read', { operation: 'get_design', nodeId: 'frame-a' });
|
|
101
106
|
assert.equal(ambiguous.isError, true); assert.match(ambiguous.content[0].text, /Ambiguous/);
|
|
102
107
|
const missing = await rpc('figma_read', { operation: 'get_design', sessionId: 'missing' });
|
|
@@ -131,9 +136,9 @@ test('multiple tabs, reconnect ownership and independent frame tasks across MCP
|
|
|
131
136
|
const recovering = rpc('figma_read', { operation: 'get_design', nodeId: 'frame-a', sessionId: 'tab-a' });
|
|
132
137
|
const ignored = await tabA.ignored.promise;
|
|
133
138
|
await plugin('tab-a', tabA); oldSocket.close();
|
|
134
|
-
assert.equal(decode(ok(await recovering)).tab, 'tab-a');
|
|
139
|
+
assert.equal(decode(ok(await recovering)).nodes[0].tab, 'tab-a');
|
|
135
140
|
await delay(30);
|
|
136
|
-
assert.equal(decode(ok(await rpc('figma_read', { operation: 'get_design', nodeId: 'frame-b', sessionId: 'tab-a' }))).id, 'frame-b');
|
|
141
|
+
assert.equal(decode(ok(await rpc('figma_read', { operation: 'get_design', nodeId: 'frame-b', sessionId: 'tab-a' }))).nodes[0].id, 'frame-b');
|
|
137
142
|
// Cross-tab response spoofing cannot consume another tab's pending request.
|
|
138
143
|
tabA.ignoreNext = true; tabA.ignored = deferred();
|
|
139
144
|
const routed = rpc('figma_read', { operation: 'get_design', nodeId: 'frame-a', sessionId: 'tab-a' });
|
|
@@ -10,7 +10,7 @@ import { parse as parseJsonc } from 'jsonc-parser';
|
|
|
10
10
|
|
|
11
11
|
function runtime(figma = {}) {
|
|
12
12
|
const context = vm.createContext({ figma, handlers: {}, console, setTimeout, Map, Set, Uint8Array });
|
|
13
|
-
for (const file of ['utils', 'token-helpers', 'handlers-read-detail', 'handlers-read', 'handlers-tokens', 'handlers-write-ops']) {
|
|
13
|
+
for (const file of ['utils', 'token-helpers', 'read-helpers', 'handlers-read-detail', 'handlers-read', 'handlers-tokens', 'handlers-write-ops', 'node-reader']) {
|
|
14
14
|
vm.runInContext(readFileSync(`plugin-src/${file}.js`, 'utf8'), context, { filename: file });
|
|
15
15
|
}
|
|
16
16
|
return context;
|
|
@@ -281,6 +281,86 @@ test('startup index defers document-wide components and coalesces duplicate scan
|
|
|
281
281
|
assert.equal(componentQueries, 1);
|
|
282
282
|
});
|
|
283
283
|
|
|
284
|
+
test('page index is shallow; frame reads paginate and instances expand only on request', async () => {
|
|
285
|
+
const messages = [];
|
|
286
|
+
let childReads = 0;
|
|
287
|
+
const page = { id: 'page', type: 'PAGE', children: [] };
|
|
288
|
+
const figma = { currentPage: page, skipInvisibleInstanceChildren: false,
|
|
289
|
+
root: { name: 'File', getPluginData: () => '' }, ui: { postMessage: msg => messages.push(msg) } };
|
|
290
|
+
const text = i => ({ id: 'text:' + i, name: 'Label ' + i, type: 'TEXT', characters: 'Content ' + i,
|
|
291
|
+
width: 15.5, height: 20.25, visible: true,
|
|
292
|
+
get fills() { assert.fail('projected index must not read paints'); },
|
|
293
|
+
get fontSize() { assert.fail('projected index must not read typography'); } });
|
|
294
|
+
const visible = Array.from({ length: 1200 }, (_, i) => text(i));
|
|
295
|
+
const hidden = text('hidden'); hidden.visible = false;
|
|
296
|
+
const frame = { id:'frame', name:'Frame', type:'FRAME', parent:page };
|
|
297
|
+
const instance = { id:'instance', name:'Instance', type:'INSTANCE', parent:frame,
|
|
298
|
+
get children() { childReads++; return figma.skipInvisibleInstanceChildren ? visible : [...visible,hidden]; } };
|
|
299
|
+
for (const node of [...visible,hidden]) node.parent = instance;
|
|
300
|
+
frame.children = [instance]; page.children = [frame];
|
|
301
|
+
figma.getNodeByIdAsync = async id => id === frame.id ? frame : id === instance.id ? instance : null;
|
|
302
|
+
const r = runtime(figma);
|
|
303
|
+
r.handlers.get_styles = async () => ({}); r.handlers.get_variables = async () => ({});
|
|
304
|
+
r.yieldToUI = async () => assert.equal(figma.skipInvisibleInstanceChildren, false);
|
|
305
|
+
const startup = await r.handlers.index_scan({ deferComponents:true });
|
|
306
|
+
assert.equal(startup.scope.id, page.id);
|
|
307
|
+
assert.equal(startup.scope.depth, 0);
|
|
308
|
+
assert.deepEqual(messages.filter(msg => msg.type === 'index-chunk').flatMap(msg => msg.nodes.map(n => n.id)), ['frame']);
|
|
309
|
+
assert.equal(childReads, 0);
|
|
310
|
+
const opaque = await r.handlers.read_nodes({ id:frame.id });
|
|
311
|
+
assert.equal(opaque.nodes.length, 2); assert.equal(opaque.nodes[1].opaque, true); assert.equal(childReads, 0);
|
|
312
|
+
messages.length = 0;
|
|
313
|
+
const result = await r.handlers.index_scan({ id:frame.id, expandInstances:true, deferComponents:true });
|
|
314
|
+
const chunks = messages.filter(msg => msg.type === 'index-chunk');
|
|
315
|
+
assert.deepEqual(chunks.map(msg => msg.nodes.length), [500,500,202]);
|
|
316
|
+
const nodes = chunks.flatMap(msg => plain(msg.nodes));
|
|
317
|
+
assert.equal(childReads, 1); assert.equal(result.nodesTruncated, false);
|
|
318
|
+
assert.equal(nodes.at(-1).content, 'Content 1199'); assert.equal(nodes.at(-1).width, 15.5);
|
|
319
|
+
assert.equal(nodes[1].childIds.length, 1200);
|
|
320
|
+
assert.equal(figma.skipInvisibleInstanceChildren, false);
|
|
321
|
+
const first = await r.handlers.read_nodes({ id:frame.id, expandInstances:true, limit:2 });
|
|
322
|
+
assert.ok(first.nextCursor); assert.equal(first.complete, false);
|
|
323
|
+
const next = await r.handlers.read_nodes({ cursor:first.nextCursor, limit:2 });
|
|
324
|
+
assert.deepEqual(plain(next.nodes.map(n => n.id)), ['text:0','text:1']);
|
|
325
|
+
await assert.rejects(r.handlers.read_nodes({ cursor:first.nextCursor }), /expired/);
|
|
326
|
+
await assert.rejects(r.handlers.read_nodes({ cursor:next.nextCursor, fields:['style'] }), /options cannot change/);
|
|
327
|
+
r.nodeRevision++;
|
|
328
|
+
await assert.rejects(r.handlers.read_nodes({ cursor:next.nextCursor }), /expired/);
|
|
329
|
+
const full = await r.handlers.read_nodes({ id:instance.id, includeHidden:true, expandInstances:true, limit:500 });
|
|
330
|
+
let all = [...full.nodes], cursor = full.nextCursor;
|
|
331
|
+
while(cursor) { const page = await r.handlers.read_nodes({cursor,limit:500}); all.push(...page.nodes); cursor=page.nextCursor; }
|
|
332
|
+
assert.equal(all.length, 1202); assert.equal(all.at(-1).visible, false);
|
|
333
|
+
assert.throws(() => r.withInstanceVisibility(false, () => { throw Error('read failed'); }), /read failed/);
|
|
334
|
+
assert.equal(figma.skipInvisibleInstanceChildren, false);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test('index yields by elapsed time and cancels when the active page changes', async () => {
|
|
338
|
+
let elapsed = 0, reads = 0, previousReads = 0, yields = 0;
|
|
339
|
+
const messages = [];
|
|
340
|
+
const page = { id: 'page', children: Array.from({ length: 30 }, (_, i) => ({
|
|
341
|
+
id: `${i}`, type: 'FRAME', get width() { elapsed += 2; reads++; return 10; },
|
|
342
|
+
})) };
|
|
343
|
+
const r = runtime({ skipInvisibleInstanceChildren: false, currentPage: page,
|
|
344
|
+
root: { name: 'File', getPluginData: () => '' }, ui: { postMessage: msg => messages.push(msg) } });
|
|
345
|
+
r.Date = { now: () => elapsed };
|
|
346
|
+
r.handlers.get_styles = async () => ({});
|
|
347
|
+
r.handlers.get_variables = async () => ({});
|
|
348
|
+
r.yieldToUI = async () => {
|
|
349
|
+
yields++;
|
|
350
|
+
assert.ok(reads - previousReads <= 4, 'yield after an 8ms slice');
|
|
351
|
+
previousReads = reads;
|
|
352
|
+
assert.equal(r.figma.skipInvisibleInstanceChildren, false);
|
|
353
|
+
};
|
|
354
|
+
await r.handlers.index_scan({ deferComponents: true });
|
|
355
|
+
assert.equal(reads, 30);
|
|
356
|
+
assert.ok(yields >= 7);
|
|
357
|
+
r.yieldToUI = async () => { r.figma.currentPage = { id: 'other', children: [] }; };
|
|
358
|
+
messages.length = 0;
|
|
359
|
+
const result = await r.handlers.index_scan({ deferComponents: true });
|
|
360
|
+
assert.equal(result.cancelled, true);
|
|
361
|
+
assert.ok(!messages.some(msg => msg.type === 'index-update'));
|
|
362
|
+
});
|
|
363
|
+
|
|
284
364
|
test('component catalogue walks pages cooperatively without bulk loading or root searches', async () => {
|
|
285
365
|
let loaded = 0, yields = 0, reads = 0, lastYieldReads = 0, maxSliceReads = 0;
|
|
286
366
|
const page = { type: 'PAGE', name: 'Components', loadAsync: async () => { loaded++; } };
|
|
@@ -334,19 +414,17 @@ test('startup subscribes to active-page changes without loading the whole file',
|
|
|
334
414
|
assert.equal(scanOptions.deferComponents, true);
|
|
335
415
|
pageListener({ nodeChanges: [{ type: 'PROPERTY_CHANGE', node: { id: 'changed:1' } }] });
|
|
336
416
|
assert.ok(r.pendingChangedNodeIds.has('changed:1'));
|
|
337
|
-
assert.ok(messages.some(msg => msg.type === '
|
|
338
|
-
|
|
339
|
-
r.
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
pageListener({ nodeChanges: [{ type: 'DELETE', id: 'deleted:1' }] });
|
|
417
|
+
assert.ok(messages.some(msg => msg.type === 'nodes-invalidated'));
|
|
418
|
+
const changed = { id:'changed:1', type:'TEXT', parent:page, width:15.5, characters:'New' };
|
|
419
|
+
r.extractDesignTree = () => assert.fail('patch sync must not extract a tree');
|
|
420
|
+
pageListener({ nodeChanges:[{ type:'PROPERTY_CHANGE', properties:['width','characters'], node:changed }] });
|
|
421
|
+
pageListener({ nodeChanges:[{ type:'DELETE', id:'deleted:1' }] });
|
|
343
422
|
await timers.filter(timer => timer.delay === 100).at(-1).callback();
|
|
344
|
-
const diff = plain(messages.filter(msg => msg.type === 'node-
|
|
345
|
-
assert.
|
|
346
|
-
assert.equal(diff.
|
|
347
|
-
assert.
|
|
348
|
-
assert.equal(
|
|
349
|
-
assert.deepEqual(diff.nodes[0].childIds, []);
|
|
423
|
+
const diff = plain(messages.filter(msg => msg.type === 'node-patch').at(-1));
|
|
424
|
+
assert.equal(diff.baseRevision, 0); assert.equal(diff.revision, 3);
|
|
425
|
+
assert.equal(diff.patches[0].values.width, 15.5); assert.equal(diff.patches[0].values.content, 'New');
|
|
426
|
+
assert.deepEqual(diff.patches[1], {kind:'delete',id:'deleted:1'});
|
|
427
|
+
assert.equal(r.nodeReadCursors.size, 0);
|
|
350
428
|
const next = { id: 'page:2', on: () => {}, off: () => {} };
|
|
351
429
|
r.figma.currentPage = next;
|
|
352
430
|
r.handlers.index_scan = async () => ({ pageId: 'page:1' });
|