test(editor): getFirstContentBlock & getLastContentBlock & isNothingSelected command (#10757)

### TL;DR

Added unit tests for block and selection commands, along with a new test helper system for creating test documents.

### What changed?

- Added unit tests for several commands:
  - `getFirstContentBlockCommand`
  - `getLastContentBlockCommand`
  - `isNothingSelectedCommand`
- Created a new test helpers make it easier to create structured test documents with a html-like syntax:

```typescript
import { describe, expect, it } from 'vitest';
import { affine } from '../__tests__/utils/affine-template';
describe('My Test', () => {
  it('should correctly handle document structure', () => {
    const doc = affine`
      <affine-page>
        <affine-note>
          <affine-paragraph>Test content</affine-paragraph>
        </affine-note>
      </affine-page>
    `;
    // Get blocks
    const pages = doc.getBlocksByFlavour('affine:page');
    const notes = doc.getBlocksByFlavour('affine:note');
    const paragraphs = doc.getBlocksByFlavour('affine:paragraph');
    expect(pages.length).toBe(1);
    expect(notes.length).toBe(1);
    expect(paragraphs.length).toBe(1);
    // Perform more tests here...
  });
});
```
This commit is contained in:
yoyoyohamapi
2025-03-14 02:35:21 +00:00
parent 04efca362e
commit d3aae962bc
9 changed files with 1040 additions and 0 deletions
@@ -0,0 +1,89 @@
/**
* @vitest-environment happy-dom
*/
import { describe, expect, it } from 'vitest';
import { getFirstContentBlockCommand } from '../../../commands/block-crud/get-first-content-block';
import { affine } from '../../helpers/affine-template';
describe('commands/block-crud', () => {
describe('getFirstContentBlockCommand', () => {
it('should return null when root is not provided and no note block exists', () => {
const host = affine`<affine-page></affine-page>`;
const [_, { firstBlock }] = host.command.exec(
getFirstContentBlockCommand,
{
root: undefined,
std: {
host,
} as any,
}
);
expect(firstBlock).toBeNull();
});
it('should return first content block when found', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
<affine-paragraph id="paragraph-1">First Paragraph</affine-paragraph>
<affine-paragraph id="paragraph-2">Second Paragraph</affine-paragraph>
</affine-note>
</affine-page>
`;
const [_, { firstBlock }] = host.command.exec(
getFirstContentBlockCommand,
{
root: undefined,
}
);
expect(firstBlock?.id).toBe('paragraph-1');
});
it('should return null when no content blocks are found in children', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
</affine-note>
</affine-page>
`;
const [_, { firstBlock }] = host.command.exec(
getFirstContentBlockCommand,
{}
);
expect(firstBlock).toBeNull();
});
it('should return first content block within specified root subtree', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
<affine-paragraph id="paragraph-1-1">1-1 Paragraph</affine-paragraph>
<affine-paragraph id="paragraph-1-2">1-2 Paragraph</affine-paragraph>
</affine-note>
<affine-note id="note-2">
<affine-paragraph id="paragraph-2-1">2-1 Paragraph</affine-paragraph>
<affine-paragraph id="paragraph-2-2">2-2 Paragraph</affine-paragraph>
</affine-note>
</affine-page>
`;
const noteBlock = host.doc.getBlock('note-2')?.model;
const [_, { firstBlock }] = host.command.exec(
getFirstContentBlockCommand,
{
root: noteBlock,
}
);
expect(firstBlock?.id).toBe('paragraph-2-1');
});
});
});
@@ -0,0 +1,80 @@
/**
* @vitest-environment happy-dom
*/
import { describe, expect, it } from 'vitest';
import { getLastContentBlockCommand } from '../../../commands/block-crud/get-last-content-block';
import { affine } from '../../helpers/affine-template';
describe('commands/block-crud', () => {
describe('getLastContentBlockCommand', () => {
it('should return null when root is not provided and no note block exists', () => {
const host = affine`<affine-page></affine-page>`;
const [_, { lastBlock }] = host.command.exec(getLastContentBlockCommand, {
root: undefined,
std: {
host,
} as any,
});
expect(lastBlock).toBeNull();
});
it('should return last content block when found', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
<affine-paragraph id="paragraph-1">First Paragraph</affine-paragraph>
<affine-paragraph id="paragraph-2">Second Paragraph</affine-paragraph>
</affine-note>
</affine-page>
`;
const [_, { lastBlock }] = host.command.exec(getLastContentBlockCommand, {
root: undefined,
});
expect(lastBlock?.id).toBe('paragraph-2');
});
it('should return null when no content blocks are found in children', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
</affine-note>
</affine-page>
`;
const [_, { lastBlock }] = host.command.exec(
getLastContentBlockCommand,
{}
);
expect(lastBlock).toBeNull();
});
it('should return last content block within specified root subtree', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
<affine-paragraph id="paragraph-1-1">1-1 Paragraph</affine-paragraph>
<affine-paragraph id="paragraph-1-2">1-2 Paragraph</affine-paragraph>
</affine-note>
<affine-note id="note-2">
<affine-paragraph id="paragraph-2-1">2-1 Paragraph</affine-paragraph>
<affine-paragraph id="paragraph-2-2">2-2 Paragraph</affine-paragraph>
</affine-note>
</affine-page>
`;
const noteBlock = host.doc.getBlock('note-2')?.model;
const [_, { lastBlock }] = host.command.exec(getLastContentBlockCommand, {
root: noteBlock,
});
expect(lastBlock?.id).toBe('paragraph-2-2');
});
});
});
@@ -0,0 +1,147 @@
/**
* @vitest-environment happy-dom
*/
import { BlockSelection, TextSelection } from '@blocksuite/block-std';
import { describe, expect, it, vi } from 'vitest';
import { isNothingSelectedCommand } from '../../../commands/selection/is-nothing-selected';
import { ImageSelection } from '../../../selection';
import { affine } from '../../helpers/affine-template';
describe('commands/selection', () => {
describe('isNothingSelectedCommand', () => {
it('should return true when nothing is selected', () => {
const host = affine`<affine-page></affine-page>`;
const [_, { isNothingSelected }] = host.command.exec(
isNothingSelectedCommand,
{}
);
expect(isNothingSelected).toBe(true);
});
it('should return false when text selection exists', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
<affine-paragraph id="paragraph-1">Test paragraph</affine-paragraph>
</affine-note>
</affine-page>
`;
// Mock text selection
const textSelection = new TextSelection({
from: {
blockId: 'paragraph-1',
index: 0,
length: 0,
},
to: {
blockId: 'paragraph-1',
index: 4,
length: 0,
},
});
const [_, { isNothingSelected }] = host.command.exec(
isNothingSelectedCommand,
{
currentTextSelection: textSelection,
}
);
expect(isNothingSelected).toBe(false);
});
it('should return false when block selection exists', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
<affine-paragraph id="paragraph-1">Test paragraph</affine-paragraph>
</affine-note>
</affine-page>
`;
// Mock block selection
const blockSelection = new BlockSelection({
blockId: 'paragraph-1',
});
const [_, { isNothingSelected }] = host.command.exec(
isNothingSelectedCommand,
{
currentBlockSelections: [blockSelection],
}
);
expect(isNothingSelected).toBe(false);
});
it('should return false when image selection exists', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
<affine-image id="image-1">Test paragraph</affine-image>
</affine-note>
</affine-page>
`;
// Mock image selection
const imageSelection = new ImageSelection({
blockId: 'image-1',
});
const [_, { isNothingSelected }] = host.command.exec(
isNothingSelectedCommand,
{
currentImageSelections: [imageSelection],
}
);
expect(isNothingSelected).toBe(false);
});
it('should return false when no selection is provided but selection is found in context', () => {
const host = affine`
<affine-page>
<affine-note id="note-1">
<affine-paragraph id="paragraph-1">Test paragraph</affine-paragraph>
</affine-note>
</affine-page>
`;
// Mock selection behavior via vi.spyOn before executing the command
const mockTextSelection = new TextSelection({
from: {
blockId: 'paragraph-1',
index: 0,
length: 0,
},
to: null,
});
const mockContext = {
// No explicit `currentTextSelection provided
std: {
selection: {
find: vi.fn().mockImplementation(type => {
if (type === TextSelection) {
return mockTextSelection;
}
return null;
}),
filter: vi.fn().mockReturnValue([]),
},
},
};
const [_, { isNothingSelected }] = host.command.exec(
isNothingSelectedCommand,
mockContext as any
);
expect(isNothingSelected).toBe(false);
});
});
});