Skip to content

Commit a8344a2

Browse files
committed
test(behavior): add behavior tests for cell color
1 parent 268d919 commit a8344a2

11 files changed

Lines changed: 663 additions & 1 deletion

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<script setup>
2+
import IconGrid from '../toolbar/IconGrid.vue';
3+
import { icons } from '../toolbar/color-dropdown-helpers.js';
4+
import { isCellSelection } from '@extensions/table/tableHelpers/isCellSelection.js';
5+
import { cellAround } from '@extensions/table/tableHelpers/cellAround.js';
6+
7+
const props = defineProps({
8+
editor: {
9+
type: Object,
10+
required: true,
11+
},
12+
closePopover: {
13+
type: Function,
14+
required: true,
15+
},
16+
});
17+
18+
// Plain object with .value — IconGridRow expects a ref-like shape (accesses .value directly).
19+
// A real ref() would be auto-unwrapped by Vue's template compiler before reaching IconGrid.
20+
const activeColor = { value: null };
21+
22+
const ensureCellSelection = () => {
23+
const { state } = props.editor;
24+
if (isCellSelection(state.selection)) return;
25+
26+
const $from = state.selection.$from;
27+
const cell = cellAround($from);
28+
if (cell) {
29+
props.editor.commands.setCellSelection({ anchorCell: cell.pos, headCell: cell.pos });
30+
}
31+
};
32+
33+
const handleSelect = (color) => {
34+
const value = color === 'none' ? null : color;
35+
ensureCellSelection();
36+
props.editor.commands.setCellBackground(value);
37+
props.closePopover();
38+
};
39+
</script>
40+
41+
<template>
42+
<IconGrid :icons="icons" :customIcons="[]" :activeColor="activeColor" :hasNoneIcon="true" @select="handleSelect" />
43+
</template>

packages/super-editor/src/components/context-menu/constants.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import copyIconSvg from '@superdoc/common/icons/copy-solid.svg?raw';
1212
import pasteIconSvg from '@superdoc/common/icons/paste-solid.svg?raw';
1313
import checkIconSvg from '@superdoc/common/icons/check-solid.svg?raw';
1414
import xMarkIconSvg from '@superdoc/common/icons/xmark-solid.svg?raw';
15+
import paintRollerIconSvg from '@superdoc/common/icons/paint-roller-solid.svg?raw';
1516

1617
export const ICONS = {
1718
addRowBefore: plusIconSvg,
@@ -35,6 +36,7 @@ export const ICONS = {
3536
removeDocumentSection: trashIconSvg,
3637
trackChangesAccept: checkIconSvg,
3738
trackChangesReject: xMarkIconSvg,
39+
cellBackground: paintRollerIconSvg,
3840
};
3941

4042
// Table actions constant
@@ -62,6 +64,7 @@ export const TEXTS = {
6264
createDocumentSection: 'Create section',
6365
trackChangesAccept: 'Accept change',
6466
trackChangesReject: 'Reject change',
67+
cellBackground: 'Cell background',
6568
};
6669

6770
export const tableActionsOptions = [

packages/super-editor/src/components/context-menu/menuItems.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import TableGrid from '../toolbar/TableGrid.vue';
22
import AIWriter from '../toolbar/AIWriter.vue';
33
import TableActions from '../toolbar/TableActions.vue';
44
import LinkInput from '../toolbar/LinkInput.vue';
5+
import CellBackgroundPicker from './CellBackgroundPicker.vue';
56
import { TEXTS, ICONS, TRIGGERS } from './constants.js';
67
import { isTrackedChangeActionAllowed } from '@extensions/track-changes/permission-helpers.js';
78
import { readClipboardRaw } from '../../core/utilities/clipboardUtils.js';
@@ -107,6 +108,8 @@ export function getItems(context, customItems = [], includeDefaultItems = true)
107108
isInTable: context.isInTable ?? false,
108109
isInSectionNode: context.isInSectionNode ?? false,
109110
isTrackedChange: context.isTrackedChange ?? false,
111+
isCellSelection: context.isCellSelection ?? false,
112+
tableSelectionKind: context.tableSelectionKind ?? null,
110113
clipboardContent: context.clipboardContent ?? { hasContent: false },
111114
selectedText: context.selectedText ?? '',
112115
hasSelection: context.hasSelection ?? Boolean(context.selectedText),
@@ -248,6 +251,16 @@ export function getItems(context, customItems = [], includeDefaultItems = true)
248251
return allowedTriggers.includes(trigger) && isInTable;
249252
},
250253
},
254+
{
255+
id: 'cell-background',
256+
label: TEXTS.cellBackground,
257+
icon: ICONS.cellBackground,
258+
component: CellBackgroundPicker,
259+
isDefault: true,
260+
showWhen: (context) => {
261+
return context.trigger === TRIGGERS.click && (context.isCellSelection || context.isInTable);
262+
},
263+
},
251264
],
252265
},
253266
{
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { mount } from '@vue/test-utils';
3+
4+
vi.mock('@extensions/table/tableHelpers/isCellSelection.js', () => ({
5+
isCellSelection: vi.fn(() => false),
6+
}));
7+
8+
vi.mock('@extensions/table/tableHelpers/cellAround.js', () => ({
9+
cellAround: vi.fn(() => null),
10+
}));
11+
12+
vi.mock('../../toolbar/IconGrid.vue', () => ({
13+
default: {
14+
props: ['icons', 'customIcons', 'activeColor', 'hasNoneIcon'],
15+
emits: ['select'],
16+
template: '<div class="icon-grid-stub" />',
17+
},
18+
}));
19+
20+
vi.mock('../../toolbar/color-dropdown-helpers.js', () => ({
21+
icons: [[{ label: 'black', value: '#000000', icon: '<svg/>', style: {} }]],
22+
}));
23+
24+
import CellBackgroundPicker from '../CellBackgroundPicker.vue';
25+
import { isCellSelection } from '@extensions/table/tableHelpers/isCellSelection.js';
26+
import { cellAround } from '@extensions/table/tableHelpers/cellAround.js';
27+
28+
describe('CellBackgroundPicker', () => {
29+
let mockEditor;
30+
let closePopover;
31+
32+
beforeEach(() => {
33+
vi.clearAllMocks();
34+
35+
closePopover = vi.fn();
36+
mockEditor = {
37+
state: {
38+
selection: {
39+
$from: { depth: 3 },
40+
},
41+
},
42+
commands: {
43+
setCellSelection: vi.fn(),
44+
setCellBackground: vi.fn(),
45+
},
46+
};
47+
});
48+
49+
function mountPicker() {
50+
return mount(CellBackgroundPicker, {
51+
props: { editor: mockEditor, closePopover },
52+
});
53+
}
54+
55+
it('should call setCellBackground directly when selection is already a CellSelection', () => {
56+
isCellSelection.mockReturnValue(true);
57+
58+
const wrapper = mountPicker();
59+
wrapper.findComponent({ name: 'IconGrid' }).vm.$emit('select', '#FF0000');
60+
61+
expect(mockEditor.commands.setCellSelection).not.toHaveBeenCalled();
62+
expect(mockEditor.commands.setCellBackground).toHaveBeenCalledWith('#FF0000');
63+
expect(closePopover).toHaveBeenCalled();
64+
});
65+
66+
it('should select the cell first when cursor is inside a cell without CellSelection', () => {
67+
isCellSelection.mockReturnValue(false);
68+
cellAround.mockReturnValue({ pos: 42 });
69+
70+
const wrapper = mountPicker();
71+
wrapper.findComponent({ name: 'IconGrid' }).vm.$emit('select', '#00FF00');
72+
73+
expect(cellAround).toHaveBeenCalledWith(mockEditor.state.selection.$from);
74+
expect(mockEditor.commands.setCellSelection).toHaveBeenCalledWith({
75+
anchorCell: 42,
76+
headCell: 42,
77+
});
78+
expect(mockEditor.commands.setCellBackground).toHaveBeenCalledWith('#00FF00');
79+
expect(closePopover).toHaveBeenCalled();
80+
});
81+
82+
it('should still attempt setCellBackground when cellAround returns null', () => {
83+
isCellSelection.mockReturnValue(false);
84+
cellAround.mockReturnValue(null);
85+
86+
const wrapper = mountPicker();
87+
wrapper.findComponent({ name: 'IconGrid' }).vm.$emit('select', '#0000FF');
88+
89+
expect(mockEditor.commands.setCellSelection).not.toHaveBeenCalled();
90+
expect(mockEditor.commands.setCellBackground).toHaveBeenCalledWith('#0000FF');
91+
expect(closePopover).toHaveBeenCalled();
92+
});
93+
94+
it('should map "none" to null for removing background', () => {
95+
isCellSelection.mockReturnValue(true);
96+
97+
const wrapper = mountPicker();
98+
wrapper.findComponent({ name: 'IconGrid' }).vm.$emit('select', 'none');
99+
100+
expect(mockEditor.commands.setCellBackground).toHaveBeenCalledWith(null);
101+
});
102+
});

packages/super-editor/src/components/context-menu/tests/menuItems.test.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ vi.mock('../constants.js', () => ({
3030
paste: 'Paste',
3131
trackChangesAccept: 'Accept Tracked Changes',
3232
trackChangesReject: 'Reject Tracked Changes',
33+
cellBackground: 'Cell background',
3334
},
3435
ICONS: {
3536
ai: '<svg>ai-icon</svg>',
@@ -40,6 +41,7 @@ vi.mock('../constants.js', () => ({
4041
cut: '<svg>cut-icon</svg>',
4142
copy: '<svg>copy-icon</svg>',
4243
paste: '<svg>paste-icon</svg>',
44+
cellBackground: '<svg>cell-background-icon</svg>',
4345
},
4446
TRIGGERS: {
4547
slash: 'slash',
@@ -51,6 +53,7 @@ vi.mock('../../toolbar/TableGrid.vue', () => ({ default: { template: '<div>Table
5153
vi.mock('../../toolbar/AIWriter.vue', () => ({ default: { template: '<div>AIWriter</div>' } }));
5254
vi.mock('../../toolbar/TableActions.vue', () => ({ default: { template: '<div>TableActions</div>' } }));
5355
vi.mock('../../toolbar/LinkInput.vue', () => ({ default: { template: '<div>LinkInput</div>' } }));
56+
vi.mock('../CellBackgroundPicker.vue', () => ({ default: { template: '<div>CellBackgroundPicker</div>' } }));
5457

5558
vi.mock('../../../core/utilities/clipboardUtils.js', () => ({
5659
readClipboardRaw: clipboardMocks.readClipboardRaw,
@@ -575,6 +578,71 @@ describe('menuItems.js', () => {
575578
});
576579
});
577580

581+
describe('getItems - cell selection context', () => {
582+
it('should show cell-background when isCellSelection is true and trigger is click', () => {
583+
mockContext = createMockContext({
584+
editor: mockEditor,
585+
trigger: TRIGGERS.click,
586+
isCellSelection: true,
587+
tableSelectionKind: 'cells',
588+
isInTable: true,
589+
});
590+
591+
const sections = getItems(mockContext);
592+
const generalSection = sections.find((s) => s.id === 'general');
593+
const cellBgItem = generalSection?.items.find((item) => item.id === 'cell-background');
594+
595+
expect(cellBgItem).toBeDefined();
596+
expect(cellBgItem.label).toBe('Cell background');
597+
});
598+
599+
it('should show cell-background when right-clicking in a table cell without CellSelection', () => {
600+
mockContext = createMockContext({
601+
editor: mockEditor,
602+
trigger: TRIGGERS.click,
603+
isCellSelection: false,
604+
isInTable: true,
605+
});
606+
607+
const sections = getItems(mockContext);
608+
const generalSection = sections.find((s) => s.id === 'general');
609+
const cellBgItem = generalSection?.items.find((item) => item.id === 'cell-background');
610+
611+
expect(cellBgItem).toBeDefined();
612+
});
613+
614+
it('should hide cell-background when not in a table at all', () => {
615+
mockContext = createMockContext({
616+
editor: mockEditor,
617+
trigger: TRIGGERS.click,
618+
isCellSelection: false,
619+
isInTable: false,
620+
});
621+
622+
const sections = getItems(mockContext);
623+
const generalSection = sections.find((s) => s.id === 'general');
624+
const cellBgItem = generalSection?.items.find((item) => item.id === 'cell-background');
625+
626+
expect(cellBgItem).toBeUndefined();
627+
});
628+
629+
it('should hide cell-background on slash trigger even with cell selection', () => {
630+
mockContext = createMockContext({
631+
editor: mockEditor,
632+
trigger: TRIGGERS.slash,
633+
isCellSelection: true,
634+
tableSelectionKind: 'row',
635+
isInTable: true,
636+
});
637+
638+
const sections = getItems(mockContext);
639+
const allItems = sections.flatMap((s) => s.items);
640+
const cellBgItem = allItems.find((item) => item.id === 'cell-background');
641+
642+
expect(cellBgItem).toBeUndefined();
643+
});
644+
});
645+
578646
describe('getItems - paste selection preservation (SD-1302)', () => {
579647
/**
580648
* Creates a mock editor with doc.content.size and selection.constructor.create

packages/super-editor/src/components/context-menu/tests/testHelpers.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,8 @@ export function createMockContext(options = {}) {
175175
activeMarks: [],
176176
isTrackedChange: false,
177177
trackedChanges: [],
178+
isCellSelection: false,
179+
tableSelectionKind: null,
178180
documentMode: 'editing',
179181
canUndo: false,
180182
canRedo: false,

0 commit comments

Comments
 (0)