Keep groups, comments and silent music in view for the agent (#429)
A live test of the music areas built a pattern, a song and a composition that stayed silent: no instrument at first, and later notes outside the C4 to B4 range of a new Notes node. The agent got no hint of either. The same test showed that new nodes left groups behind, that a comment could end up under other nodes, and that an end node like Output stayed left of the devices put in front of it. - get_asset lists under silent what of a pattern, song or composition is not heard. That is a missing instrument, an instrument that sounds no note, or notes outside what its Notes nodes take in, in a song after the transpose. - soundingNotes moves from the pattern tab to src/core/instrument/compile.ts. The tab and the agent use the same rule. - When new nodes go between two that have no room, everything from the fed node on moves right together. This holds for Arrange with a selection too, so no link runs backwards. - A new node linked to a node in a group joins that group, and the group grows in the same undo step. A group that would take in a foreign node stays as it is. - Arrange takes groups and comments along: a group whose nodes all moved the same keeps its size, otherwise it is fitted anew. The rules live in the new src/editor/graph/annotationFollow.ts. - add_comment and fit_group put a comment below its nodes when a node or a group title is in the way above. lint_graph counts covered comments as coveredNotes. - add_notes, get_asset, lint_graph and the relay instructions name the default range and how to mend a covered comment. Tests: - graph-follow (new, 12), agent-edit, agent-music (11) and the other agent, graph, pattern and relay tests green - 10 deliberate breakages, each one red in its own test - e2e 71: 25/25 - Gate: CHECK EXIT 0, 324 files, 5002 tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
10f5e3ba8f
commit
19902a9f02
16 files changed
+923
-119
No files matched your search
@@ -195,6 +195,52 @@ describe('run_commands', () => {
|
||||
expect(text(unknown)).toContain('Step 1 (arrange_graph): Jump has no node nope');
|
||||
});
|
||||
|
||||
it('puts a new node linked into a group into it, the group growing in the same undo step', async () => {
|
||||
const fixture = makeSpriteScene();
|
||||
const behavior = makeBehavior(fixture, 'Jump');
|
||||
const { host } = hostFor(fixture);
|
||||
const link = (from: string, fromPort: string, to: string, toPort: string) => ({
|
||||
command: 'connect',
|
||||
args: { asset: behavior.id, from: { node: from, port: fromPort }, to: { node: to, port: toPort } },
|
||||
});
|
||||
await runTool(
|
||||
'run_commands',
|
||||
{
|
||||
steps: [
|
||||
{ command: 'add_node', args: { asset: behavior.id, node: 'OnStart' }, as: 'start' },
|
||||
{ command: 'add_node', args: { asset: behavior.id, node: 'Move' }, as: 'move' },
|
||||
link('$start', 'exec', '$move', 'exec'),
|
||||
{ command: 'add_group', args: { asset: behavior.id, nodes: ['$start', '$move'], title: 'Jump' } },
|
||||
],
|
||||
},
|
||||
host,
|
||||
);
|
||||
const [start, move] = behavior.graph.nodes;
|
||||
const before = { ...behavior.graph.groups![0]! };
|
||||
const result = await runTool(
|
||||
'run_commands',
|
||||
{
|
||||
steps: [
|
||||
{ command: 'add_node', args: { asset: behavior.id, node: 'Owner' }, as: 'owner' },
|
||||
link('$owner', 'object', move!.id, 'object'),
|
||||
],
|
||||
},
|
||||
host,
|
||||
);
|
||||
expect(result.isError).toBeFalsy();
|
||||
const owner = behavior.graph.nodes[2]!;
|
||||
const group = behavior.graph.groups![0]!;
|
||||
expect(owner.x + NODE_WIDTH + COLUMN_GAP).toBe(move!.x);
|
||||
expect(owner.x + NODE_WIDTH / 2).toBeGreaterThan(group.x);
|
||||
expect(owner.y).toBeGreaterThan(group.y);
|
||||
expect(owner.y).toBeLessThan(group.y + group.h);
|
||||
// Under the start, where the group had room already.
|
||||
expect(group.h).toBeGreaterThanOrEqual(before.h);
|
||||
expect([start!.x, start!.y]).toEqual([behavior.graph.nodes[0]!.x, behavior.graph.nodes[0]!.y]);
|
||||
await runTool('undo', {}, host);
|
||||
expect(behavior.graph.groups![0]).toEqual(before);
|
||||
});
|
||||
|
||||
it('changes nothing when a step fails, and names the step and why', async () => {
|
||||
const fixture = makeSpriteScene();
|
||||
const { host } = hostFor(fixture);
|
||||
|
||||
@@ -423,3 +423,102 @@ describe('compositions', () => {
|
||||
expect(last.text).toContain('keeps at least one track');
|
||||
});
|
||||
});
|
||||
|
||||
describe('silent music', () => {
|
||||
it('get_asset says what plays nothing for want of an instrument, until it has one', async () => {
|
||||
const fixture = makeSpriteScene();
|
||||
const { Riff, Theme, Cut, Lead } = await made(fixture, {
|
||||
Riff: 'pattern',
|
||||
Theme: 'song',
|
||||
Cut: 'composition',
|
||||
Lead: 'instrument',
|
||||
});
|
||||
const track = byId<SongAsset>(fixture, Theme).tracks[0]!.id;
|
||||
const lane = byId<CompositionAsset>(fixture, Cut).tracks[0]!.id;
|
||||
const built = await run(fixture, [
|
||||
{ command: 'add_song_item', args: { asset: Theme, track, play: Riff, tick: 0 }, as: 'item' },
|
||||
{ command: 'add_composition_item', args: { asset: Cut, track: lane, place: Riff, start: 0 } },
|
||||
]);
|
||||
expect(built.result.isError).toBeUndefined();
|
||||
const item = byId<SongAsset>(fixture, Theme).items[0]!.id;
|
||||
expect((await read(fixture, Riff)).silent).toEqual([
|
||||
'It has no instrument and plays nothing: set the property instrument of the pattern to an Instrument asset.',
|
||||
]);
|
||||
expect((await read(fixture, Theme)).silent).toEqual([expect.stringContaining(`Item ${item} plays Riff`)]);
|
||||
expect((await read(fixture, Cut)).silent).toEqual([expect.stringContaining('which is not all heard')]);
|
||||
|
||||
// The item's own instrument is enough for the song; the pattern's for all of them.
|
||||
await run(fixture, [{ command: 'change_song_item', args: { asset: Theme, item, instrument: Lead } }]);
|
||||
expect((await read(fixture, Theme)).silent).toBeUndefined();
|
||||
expect((await read(fixture, Riff)).silent).toHaveLength(1);
|
||||
await run(fixture, [
|
||||
{ command: 'set_asset_property', args: { asset: Riff, key: 'instrument', value: Lead } },
|
||||
]);
|
||||
for (const asset of [Riff, Theme, Cut]) expect((await read(fixture, asset)).silent).toBeUndefined();
|
||||
});
|
||||
|
||||
it('names the notes outside what the instrument sounds, after the transpose, until its range takes them in', async () => {
|
||||
const fixture = makeSpriteScene();
|
||||
const { Riff, Theme, Cut, Lead } = await made(fixture, {
|
||||
Riff: 'pattern',
|
||||
Theme: 'song',
|
||||
Cut: 'composition',
|
||||
Lead: 'instrument',
|
||||
});
|
||||
const node = (type: string) =>
|
||||
(
|
||||
fixture.project.assets.find((a) => a.id === Lead) as {
|
||||
graph: { nodes: { id: string; type: string }[] };
|
||||
}
|
||||
).graph.nodes.find((n) => n.type === type)!.id;
|
||||
const track = byId<SongAsset>(fixture, Theme).tracks[0]!.id;
|
||||
const lane = byId<CompositionAsset>(fixture, Cut).tracks[0]!.id;
|
||||
const built = await run(fixture, [
|
||||
{ command: 'set_asset_property', args: { asset: Riff, key: 'instrument', value: Lead } },
|
||||
{
|
||||
command: 'add_notes',
|
||||
args: {
|
||||
asset: Riff,
|
||||
notes: [
|
||||
{ tick: 0, length: 480, note: 'C4' },
|
||||
{ tick: 480, length: 480, note: 'C5' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{ command: 'add_song_item', args: { asset: Theme, track, play: Riff, tick: 0, transpose: -12 } },
|
||||
{ command: 'add_composition_item', args: { asset: Cut, track: lane, place: Riff, start: 0 } },
|
||||
]);
|
||||
expect(built.result.isError).toBeUndefined();
|
||||
const item = byId<SongAsset>(fixture, Theme).items[0]!.id;
|
||||
|
||||
// A new instrument's Sample waits for its sound: no note is heard, and get_graph says why.
|
||||
expect((await read(fixture, Riff)).silent).toEqual([
|
||||
'C4, C5 are not heard: Lead sounds no note at all. To hear them, get_graph on it shows what it lacks, like the sound of a Sample, or move the notes.',
|
||||
]);
|
||||
|
||||
const sound = addSound(fixture);
|
||||
await run(fixture, [
|
||||
{
|
||||
command: 'set_node_param',
|
||||
args: { asset: Lead, node: node('InstrumentSample'), key: 'sound', value: sound },
|
||||
},
|
||||
]);
|
||||
expect((await read(fixture, Riff)).silent).toEqual([
|
||||
'C5 is not heard: Lead sounds only C4 to B4. To hear them, widen "from" and "to" of a Notes node in it, or move the notes.',
|
||||
]);
|
||||
// An octave down, C5 comes in and C4 drops out: the song goes by the notes as they sound.
|
||||
expect((await read(fixture, Theme)).silent).toEqual([
|
||||
`Item ${item} plays Riff, transposed by -12. C3 is not heard: Lead sounds only C4 to B4. To hear them, widen "from" and "to" of a Notes node in it, or change the transpose.`,
|
||||
]);
|
||||
expect((await read(fixture, Cut)).silent).toEqual([
|
||||
expect.stringContaining('Riff, which is not all heard'),
|
||||
]);
|
||||
|
||||
const notes = node('InstrumentNotes');
|
||||
await run(fixture, [
|
||||
{ command: 'set_node_param', args: { asset: Lead, node: notes, key: 'from', value: '48' } },
|
||||
{ command: 'set_node_param', args: { asset: Lead, node: notes, key: 'to', value: '72' } },
|
||||
]);
|
||||
for (const asset of [Riff, Theme, Cut]) expect((await read(fixture, asset)).silent).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -141,7 +141,7 @@ describe('arrange', () => {
|
||||
node('new-b', ['exec'], [], 30, 20),
|
||||
],
|
||||
edges: [edge('kept', 'exec', 'new-a', 'exec'), edge('new-a', 'exec', 'new-b', 'exec')],
|
||||
boxes: [{ id: 'group', x: 200, y: -40, w: 300, h: 200 }],
|
||||
boxes: [{ id: 'group', kind: 'group', x: 200, y: -40, w: 300, h: 200 }],
|
||||
};
|
||||
const result = placed(graph, new Set(['new-a', 'new-b']));
|
||||
const kept = result.nodes.find((n) => n.id === 'kept')!;
|
||||
@@ -226,7 +226,7 @@ describe('arrange', () => {
|
||||
const graph: LayoutGraph = {
|
||||
nodes: [node('a', [], ['exec'], 220, 0), node('b', ['exec'], [], 30, 20)],
|
||||
edges: [edge('a', 'exec', 'b', 'exec')],
|
||||
boxes: [{ id: 'group', x: 0, y: -40, w: 700, h: 300 }],
|
||||
boxes: [{ id: 'group', kind: 'group', x: 0, y: -40, w: 700, h: 300 }],
|
||||
};
|
||||
const result = placed(graph);
|
||||
expect(Math.min(...result.nodes.map((n) => n.x))).toBe(30);
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
/**
|
||||
* Groups and comments going along when nodes are arranged (#429), new nodes joining the group of
|
||||
* what they link to, room made for nodes put between, and comments kept clear of other nodes.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { freeNoteBox, groupBoxAround, noteBoxAbove } from '../../src/editor/graph/annotationDrag';
|
||||
import { adoptingGroups, followedBoxes } from '../../src/editor/graph/annotationFollow';
|
||||
import {
|
||||
arrange,
|
||||
COLUMN_GAP,
|
||||
type LayoutBox,
|
||||
type LayoutEdge,
|
||||
type LayoutGraph,
|
||||
type LayoutNode,
|
||||
lintLayout,
|
||||
} from '../../src/editor/graph/arrange';
|
||||
|
||||
function node(id: string, inputs: string[], outputs: string[], x = 0, y = 0): LayoutNode {
|
||||
const rows = (keys: string[]) => Object.fromEntries(keys.map((key, i) => [key, 30 + i * 20 + 10]));
|
||||
return {
|
||||
id,
|
||||
x,
|
||||
y,
|
||||
w: 176,
|
||||
h: 36 + Math.max(inputs.length, outputs.length) * 20,
|
||||
inputs: rows(inputs),
|
||||
outputs: rows(outputs),
|
||||
};
|
||||
}
|
||||
|
||||
let edgeCount = 0;
|
||||
function edge(from: string, fromPort: string, to: string, toPort: string): LayoutEdge {
|
||||
edgeCount += 1;
|
||||
return {
|
||||
id: `e${edgeCount}`,
|
||||
from: { node: from, port: fromPort },
|
||||
to: { node: to, port: toPort },
|
||||
exec: fromPort === 'exec',
|
||||
};
|
||||
}
|
||||
|
||||
/** Arranges as the editor does: the nodes, then the groups and comments after them. */
|
||||
function placed(graph: LayoutGraph, movable?: ReadonlySet<string>, fresh = false): LayoutGraph {
|
||||
const joining = fresh && movable ? adoptingGroups(graph, movable) : [];
|
||||
const spots = arrange(graph, movable, { open: new Set(joining), fresh });
|
||||
const boxes = followedBoxes(graph, spots, { groups: joining, nodes: [...(movable ?? [])] });
|
||||
return {
|
||||
...graph,
|
||||
nodes: graph.nodes.map((n) => ({ ...n, ...(spots.get(n.id) ?? {}) })),
|
||||
boxes: graph.boxes.map((b) => ({ ...b, ...boxes.get(b.id)! })),
|
||||
};
|
||||
}
|
||||
|
||||
const at = (graph: LayoutGraph, id: string) => graph.nodes.find((n) => n.id === id)!;
|
||||
const box = (graph: LayoutGraph, id: string) => graph.boxes.find((b) => b.id === id)!;
|
||||
const centredIn = (b: LayoutBox, n: LayoutNode) =>
|
||||
n.x + n.w / 2 >= b.x && n.x + n.w / 2 <= b.x + b.w && n.y + n.h / 2 >= b.y && n.y + n.h / 2 <= b.y + b.h;
|
||||
|
||||
/** The walking graph of #429 in its group, with the comment above it, as the user has it. */
|
||||
function walking(): LayoutGraph {
|
||||
const nodes = [
|
||||
node('update', [], ['exec', 'dt'], 0, 0),
|
||||
node('owner', [], ['object'], 0, 100),
|
||||
node('axis', [], ['x', 'y', 'active'], 0, 180),
|
||||
node('move', ['exec', 'object', 'dx', 'dy', 'scale', 'dt'], ['exec'], 248, 0),
|
||||
];
|
||||
const group = { id: 'group', kind: 'group' as const, ...groupBoxAround(nodes)! };
|
||||
const note = { id: 'note', kind: 'note' as const, ...noteBoxAbove(nodes)! };
|
||||
return {
|
||||
nodes,
|
||||
edges: [
|
||||
edge('update', 'exec', 'move', 'exec'),
|
||||
edge('update', 'dt', 'move', 'dt'),
|
||||
edge('owner', 'object', 'move', 'object'),
|
||||
edge('axis', 'x', 'move', 'dx'),
|
||||
edge('axis', 'y', 'move', 'dy'),
|
||||
],
|
||||
boxes: [group, note],
|
||||
};
|
||||
}
|
||||
|
||||
describe('new nodes and groups', () => {
|
||||
it('puts a new node feeding one in a group into that group, which grows round it', () => {
|
||||
const graph = walking();
|
||||
graph.nodes.push(node('vars', [], ['speed'], 508, 0));
|
||||
graph.edges.push(edge('vars', 'speed', 'move', 'scale'));
|
||||
expect(adoptingGroups(graph, new Set(['vars']))).toEqual(['group']);
|
||||
const result = placed(graph, new Set(['vars']), true);
|
||||
const group = box(result, 'group');
|
||||
for (const n of result.nodes) expect(centredIn(group, n)).toBe(true);
|
||||
// It grew down for the new node only; the nodes there stay, and the comment stays above it.
|
||||
expect(group.y).toBe(box(graph, 'group').y);
|
||||
expect(group.h).toBeGreaterThan(box(graph, 'group').h);
|
||||
expect(box(result, 'note')).toEqual(box(graph, 'note'));
|
||||
expect(lintLayout(result)).toMatchObject({ overlaps: 0, backwards: 0, coveredNotes: 0, crossings: 4 });
|
||||
});
|
||||
|
||||
it('leaves a group as it is when growing round the new node would take in a node not its own', () => {
|
||||
const graph = walking();
|
||||
graph.nodes.push(node('vars', [], ['speed'], 508, 0));
|
||||
graph.edges.push(edge('vars', 'speed', 'move', 'scale'));
|
||||
// A stranger right under the group, where the new node would go.
|
||||
const group = box(graph, 'group');
|
||||
graph.nodes.push(node('stranger', [], [], 0, group.y + group.h + 8));
|
||||
const result = placed(graph, new Set(['vars']), true);
|
||||
expect(box(result, 'group')).toEqual(group);
|
||||
});
|
||||
|
||||
it('puts a new node taking from one in a group next to it, not where it waited far off', () => {
|
||||
const graph = walking();
|
||||
graph.nodes.push(node('far', [], [], 2000, 400));
|
||||
// Where the agent's new node waits: right of everything.
|
||||
graph.nodes.push(node('after', ['exec'], [], 2260, 0));
|
||||
graph.edges.push(edge('move', 'exec', 'after', 'exec'));
|
||||
const result = placed(graph, new Set(['after']), true);
|
||||
const move = at(result, 'move');
|
||||
expect(at(result, 'after').x).toBe(move.x + move.w + COLUMN_GAP);
|
||||
expect(centredIn(box(result, 'group'), at(result, 'after'))).toBe(true);
|
||||
expect(centredIn(box(result, 'group'), at(result, 'far'))).toBe(false);
|
||||
});
|
||||
|
||||
it('takes nodes in only while none of the new ones stands in a group already', () => {
|
||||
const graph = walking();
|
||||
graph.nodes.push(node('vars', [], ['speed'], 40, 60));
|
||||
graph.edges.push(edge('vars', 'speed', 'move', 'scale'));
|
||||
expect(adoptingGroups(graph, new Set(['vars']))).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('arranging with groups and comments', () => {
|
||||
it('fits a group round its nodes again after Arrange, with its comment going along', () => {
|
||||
const graph = walking();
|
||||
// In one row, as the agent leaves new nodes, and grouped like that.
|
||||
graph.nodes.forEach((n, i) => Object.assign(n, { x: i * 260, y: 0 }));
|
||||
graph.boxes[0] = { id: 'group', kind: 'group', ...groupBoxAround(graph.nodes)! };
|
||||
graph.boxes[1] = { id: 'note', kind: 'note', ...noteBoxAbove(graph.nodes)! };
|
||||
const result = placed(graph);
|
||||
const group = box(result, 'group');
|
||||
expect(group).toMatchObject(groupBoxAround(result.nodes)!);
|
||||
expect(group.w).toBeLessThan(box(graph, 'group').w / 2);
|
||||
const note = box(result, 'note');
|
||||
// Just above the group, as far from it as before.
|
||||
expect(group.y - (note.y + note.h)).toBe(
|
||||
box(graph, 'group').y - (box(graph, 'note').y + box(graph, 'note').h),
|
||||
);
|
||||
expect(lintLayout(result).coveredNotes).toBe(0);
|
||||
});
|
||||
|
||||
it('moves a group whose nodes all moved alike as it is, keeping its size', () => {
|
||||
const graph: LayoutGraph = {
|
||||
nodes: [node('a', [], ['exec'], 0, 0), node('b', ['exec'], [], 248, 0)],
|
||||
edges: [edge('a', 'exec', 'b', 'exec')],
|
||||
boxes: [{ id: 'roomy', kind: 'group', x: -100, y: -100, w: 700, h: 400 }],
|
||||
};
|
||||
const spots = new Map([
|
||||
['a', { x: 50, y: 30 }],
|
||||
['b', { x: 298, y: 30 }],
|
||||
]);
|
||||
expect(followedBoxes(graph, spots).get('roomy')).toEqual({ x: -50, y: -70, w: 700, h: 400 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('room for nodes put between', () => {
|
||||
/** A fresh instrument: Notes and Output close together, and the chain the agent puts between. */
|
||||
function instrument(): LayoutGraph {
|
||||
return {
|
||||
nodes: [
|
||||
node('notes', [], ['note'], 0, 0),
|
||||
node('output', ['audio'], [], 248, 0),
|
||||
node('beside', [], [], 400, 200),
|
||||
node('chip', ['note'], ['audio'], 600, 0),
|
||||
node('env', ['audio'], ['audio'], 860, 0),
|
||||
node('delay', ['audio'], ['audio'], 1120, 0),
|
||||
],
|
||||
edges: [
|
||||
edge('notes', 'note', 'chip', 'note'),
|
||||
edge('chip', 'audio', 'env', 'audio'),
|
||||
edge('env', 'audio', 'delay', 'audio'),
|
||||
edge('delay', 'audio', 'output', 'audio'),
|
||||
],
|
||||
boxes: [{ id: 'about', kind: 'note', x: 248, y: -120, w: 220, h: 96 }],
|
||||
};
|
||||
}
|
||||
|
||||
it('pushes what they feed, and everything right of it, far enough right', () => {
|
||||
const graph = instrument();
|
||||
const result = placed(graph, new Set(['chip', 'env', 'delay']), true);
|
||||
expect(lintLayout(result)).toMatchObject({ overlaps: 0, backwards: 0, crossings: 0 });
|
||||
const notes = at(result, 'notes');
|
||||
const delay = at(result, 'delay');
|
||||
expect(at(result, 'chip').x).toBe(notes.x + notes.w + COLUMN_GAP);
|
||||
expect(at(result, 'output').x).toBe(delay.x + delay.w + COLUMN_GAP);
|
||||
expect([notes.x, notes.y]).toEqual([0, 0]);
|
||||
// What stood right of the output moved by as much, and so did the comment on it.
|
||||
const by = at(result, 'output').x - 248;
|
||||
expect(at(result, 'beside')).toMatchObject({ x: 400 + by, y: 200 });
|
||||
expect(box(result, 'about')).toMatchObject({ x: 248 + by, y: -120 });
|
||||
});
|
||||
|
||||
it('pushes nothing where there is room between already', () => {
|
||||
const graph = instrument();
|
||||
at(graph, 'output').x = 1500;
|
||||
const result = placed(graph, new Set(['chip', 'env', 'delay']), true);
|
||||
expect(at(result, 'output').x).toBe(1500);
|
||||
expect(at(result, 'beside').x).toBe(400);
|
||||
expect(lintLayout(result)).toMatchObject({ overlaps: 0, backwards: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('freeNoteBox', () => {
|
||||
const targets = [{ id: 'a', x: 0, y: 200, w: 176, h: 76 }];
|
||||
const above = noteBoxAbove(targets)!;
|
||||
const empty = { nodes: [], groups: [], notes: [] };
|
||||
|
||||
it('puts a comment above its nodes while that is free', () => {
|
||||
expect(freeNoteBox(targets, empty)).toEqual(above);
|
||||
});
|
||||
|
||||
it('puts it below them where a node stands above, and above again when below is taken too', () => {
|
||||
const over = { x: 0, y: above.y + 10, w: 176, h: 40 };
|
||||
const below = freeNoteBox(targets, { ...empty, nodes: [over] })!;
|
||||
expect(below.y).toBe(200 + 76 + 40);
|
||||
const under = { x: 0, y: below.y + 10, w: 176, h: 40 };
|
||||
expect(freeNoteBox(targets, { ...empty, nodes: [over, under] })).toEqual(above);
|
||||
});
|
||||
|
||||
it('may lie inside a group, but not over its title bar or across its edge', () => {
|
||||
const roomy = { x: -50, y: above.y - 50, w: 400, h: 500 };
|
||||
expect(freeNoteBox(targets, { ...empty, groups: [roomy] })).toEqual(above);
|
||||
const titled = { ...roomy, y: above.y - 10 };
|
||||
expect(freeNoteBox(targets, { ...empty, groups: [titled] })!.y).toBe(200 + 76 + 40);
|
||||
});
|
||||
});
|
||||
|
||||
describe('lintLayout', () => {
|
||||
it('counts a comment a node lies on', () => {
|
||||
const graph: LayoutGraph = {
|
||||
nodes: [node('a', [], [], 0, 0), node('b', [], [], 0, 300)],
|
||||
edges: [],
|
||||
boxes: [
|
||||
{ id: 'hidden', kind: 'note', x: 50, y: 20, w: 220, h: 96 },
|
||||
{ id: 'clear', kind: 'note', x: 0, y: 150, w: 220, h: 96 },
|
||||
{ id: 'group', kind: 'group', x: -20, y: -44, w: 216, h: 120 },
|
||||
],
|
||||
};
|
||||
const lint = lintLayout(graph);
|
||||
expect(lint.coveredNotes).toBe(1);
|
||||
expect(lint.examples).toContainEqual({ kind: 'covered', ids: ['hidden', 'a'] });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user