Text-Vergleich, &-Verkettung und benannte Bereiche
- Vergleichsoperatoren (= <> > < >= <=) unterscheiden jetzt Text- von Zahlenvergleich statt jede Seite zwangsweise durch toNumber() zu schicken (=A1="Nord" gab vorher #VALUE! statt 1); ermöglicht Text-Kriterien in selbstgebauten COUNTIF/SUMIF/AVERAGEIF-Funktionen (neue README-Sektion "SUMIF/COUNTIF/AVERAGEIF nachbauen"). - Neuer &-Operator (Excel-Präzedenz zwischen Vergleich und +/-) plus CONCAT/CONCATENATE als Funktions-Äquivalent. - Benannte Bereiche: neuer Toolbar-Button "🏷" verwaltet dokumentweite Namen für eine feste Zelle/einen festen Bereich (z. B. Umsatz_Q1statt Umsatz!B2:B6), unqualifiziert in jeder Top-Level-Formel nutzbar. Live gegen die Tabellen-ID aufgelöst (übersteht Tabellen-Umbenennung), bewusst nicht innerhalb einer benutzerdefinierten Funktion selbst nutzbar (Aufrufer können sie aber als Argument übergeben). - Nebenbei: COUNT2s Autovervollständigungs-Hint korrigiert (zeigte "COUNT(...)" statt "COUNT2(...)"). 374 Tests, vue-tsc/eslint/build sauber, Playwright-live-verifiziert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -871,6 +871,138 @@ describe('custom functions (document-level library)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('named ranges (document-level names for a fixed cell/range)', () => {
|
||||
it('addNamedRange stores the definition and a formula elsewhere resolves it unqualified', () => {
|
||||
const s = fresh();
|
||||
const id = s.addTable(s.doc.activeSheetId);
|
||||
const name = s.tableById(id)!.name;
|
||||
setCell(s, name, 'B2', '42');
|
||||
|
||||
const result = s.addNamedRange('Preis', id, coords('B2'), coords('B2'));
|
||||
expect(typeof result).toBe('string'); // the new range's id, not { error }
|
||||
expect(s.doc.namedRanges.find((r) => r.name === 'Preis')).toMatchObject({ tableId: id });
|
||||
|
||||
setCell(s, name, 'A1', '=Preis + 1');
|
||||
expect(val(s, name, 'A1')).toBe(43);
|
||||
});
|
||||
|
||||
it('a multi-cell named range works as a range function argument', () => {
|
||||
const s = fresh();
|
||||
const id = s.addTable(s.doc.activeSheetId);
|
||||
const name = s.tableById(id)!.name;
|
||||
setCell(s, name, 'B2', '10');
|
||||
setCell(s, name, 'B3', '20');
|
||||
s.addNamedRange('Bereich', id, coords('B2'), coords('B3'));
|
||||
setCell(s, name, 'A1', '=SUM(Bereich)');
|
||||
expect(val(s, name, 'A1')).toBe(30);
|
||||
});
|
||||
|
||||
it('rejects a name colliding with a built-in function', () => {
|
||||
const s = fresh();
|
||||
const id = s.addTable(s.doc.activeSheetId);
|
||||
const result = s.addNamedRange('SUM', id, coords('B2'), coords('B2')) as { error: string };
|
||||
expect(result.error).toBeTruthy();
|
||||
});
|
||||
|
||||
it('rejects a name colliding with an existing custom function', () => {
|
||||
const s = fresh();
|
||||
const id = s.addTable(s.doc.activeSheetId);
|
||||
s.addCustomFunction('MARGE', ['x'], 'x');
|
||||
const result = s.addNamedRange('MARGE', id, coords('B2'), coords('B2')) as { error: string };
|
||||
expect(result.error).toBeTruthy();
|
||||
});
|
||||
|
||||
it('rejects a name colliding with another named range (case-insensitive)', () => {
|
||||
const s = fresh();
|
||||
const id = s.addTable(s.doc.activeSheetId);
|
||||
s.addNamedRange('Preis', id, coords('B2'), coords('B2'));
|
||||
const result = s.addNamedRange('preis', id, coords('C2'), coords('C2')) as { error: string };
|
||||
expect(result.error).toBeTruthy();
|
||||
});
|
||||
|
||||
it('rejects a name that looks like a cell address', () => {
|
||||
const s = fresh();
|
||||
const id = s.addTable(s.doc.activeSheetId);
|
||||
const result = s.addNamedRange('B2', id, coords('C2'), coords('C2')) as { error: string };
|
||||
expect(result.error).toBeTruthy();
|
||||
});
|
||||
|
||||
it('updateNamedRange changes name/coords and existing formulas re-resolve', () => {
|
||||
const s = fresh();
|
||||
const id = s.addTable(s.doc.activeSheetId);
|
||||
const name = s.tableById(id)!.name;
|
||||
setCell(s, name, 'B2', '10');
|
||||
setCell(s, name, 'C2', '99');
|
||||
const rid = s.addNamedRange('Preis', id, coords('B2'), coords('B2')) as string;
|
||||
setCell(s, name, 'A1', '=Preis');
|
||||
expect(val(s, name, 'A1')).toBe(10);
|
||||
|
||||
s.updateNamedRange(rid, 'Preis', id, coords('C2'), coords('C2'));
|
||||
expect(val(s, name, 'A1')).toBe(99);
|
||||
});
|
||||
|
||||
it('deleteNamedRange removes the definition; existing formulas become #NAME?', () => {
|
||||
const s = fresh();
|
||||
const id = s.addTable(s.doc.activeSheetId);
|
||||
const name = s.tableById(id)!.name;
|
||||
const before = s.doc.namedRanges.length; // the seed already ships one ("Umsatz_Q1")
|
||||
setCell(s, name, 'B2', '10');
|
||||
const rid = s.addNamedRange('Preis', id, coords('B2'), coords('B2')) as string;
|
||||
setCell(s, name, 'A1', '=Preis');
|
||||
expect(val(s, name, 'A1')).toBe(10);
|
||||
|
||||
s.deleteNamedRange(rid);
|
||||
expect(s.doc.namedRanges).toHaveLength(before);
|
||||
expect(s.doc.namedRanges.find((r) => r.id === rid)).toBeUndefined();
|
||||
expect(isError(val(s, name, 'A1'))).toBe(true);
|
||||
});
|
||||
|
||||
it('survives its target table being renamed', () => {
|
||||
const s = fresh();
|
||||
const id = s.addTable(s.doc.activeSheetId);
|
||||
const name = s.tableById(id)!.name;
|
||||
setCell(s, name, 'B2', '10');
|
||||
s.addNamedRange('Preis', id, coords('B2'), coords('B2'));
|
||||
setCell(s, name, 'A1', '=Preis');
|
||||
expect(val(s, name, 'A1')).toBe(10);
|
||||
|
||||
s.renameObject(id, 'Verkauf');
|
||||
expect(val(s, 'Verkauf', 'A1')).toBe(10); // still resolves, no #NAME?/#REF
|
||||
});
|
||||
|
||||
it('undo/redo covers a named-range change', () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const s = fresh();
|
||||
const id = s.addTable(s.doc.activeSheetId);
|
||||
vi.advanceTimersByTime(600); // let addTable settle into its own history step first
|
||||
const before = s.doc.namedRanges.length; // the seed already ships one ("Umsatz_Q1")
|
||||
|
||||
s.addNamedRange('Preis', id, coords('B2'), coords('B2'));
|
||||
vi.advanceTimersByTime(600);
|
||||
expect(s.doc.namedRanges).toHaveLength(before + 1);
|
||||
|
||||
s.undo();
|
||||
expect(s.doc.namedRanges).toHaveLength(before);
|
||||
s.redo();
|
||||
expect(s.doc.namedRanges).toHaveLength(before + 1);
|
||||
expect(s.doc.namedRanges.find((r) => r.name === 'Preis')).toBeTruthy();
|
||||
} finally {
|
||||
vi.runOnlyPendingTimers();
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('JSON export/import round-trips namedRanges losslessly', () => {
|
||||
const s = fresh();
|
||||
const id = s.addTable(s.doc.activeSheetId);
|
||||
s.addNamedRange('Preis', id, coords('B2'), coords('B2'));
|
||||
const before = JSON.stringify(s.doc.namedRanges);
|
||||
s.replaceDocument(parseDocument(serializeDocument(s.doc)));
|
||||
expect(JSON.stringify(s.doc.namedRanges)).toBe(before);
|
||||
});
|
||||
});
|
||||
|
||||
describe('functionSuggestions (formula autocomplete)', () => {
|
||||
it('filters built-ins by prefix, case-insensitively', () => {
|
||||
const s = fresh();
|
||||
|
||||
Reference in New Issue
Block a user