1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-27 01:37:36 +00:00

LibTest+Spreadsheet: Add some basic spreadsheet runtime behaviour tests

As there's a somewhat active development going on, let's keep the
expected behaviour under tests to make sure nothing blows up :^)
This commit is contained in:
Ali Mohammad Pur 2022-02-22 07:49:04 +03:30 committed by Ali Mohammad Pur
parent 0fe97cdfe4
commit bed129a69f
13 changed files with 578 additions and 12 deletions

View file

@ -0,0 +1,97 @@
describe("Position", () => {
test("here", () => {
const workbook = createWorkbook();
const sheet = createSheet(workbook, "Sheet 1");
sheet.makeCurrent();
sheet.setCell("A", 0, "0");
sheet.focusCell("A", 0);
expect(here).toBeDefined();
let position = here();
expect(position).toBeDefined();
expect(position.column).toEqual("A");
expect(position.name).toEqual("A0");
expect(position.row).toEqual(0);
expect(position.sheet).toBe(sheet);
expect(position.contents).toEqual("0");
expect(position.value()).toEqual("0");
expect(position.toString()).toEqual("<Cell at A0>");
position.contents = "=1 + 1";
expect(position.contents).toEqual("=1 + 1");
expect(position.value()).toEqual(2);
expect(position.up().row).toEqual(0);
expect(position.down().row).toEqual(1);
expect(position.right().row).toEqual(0);
expect(position.left().row).toEqual(0);
sheet.addColumn("B");
expect(position.up().column).toEqual("A");
expect(position.down().column).toEqual("A");
expect(position.right().column).toEqual("B");
expect(position.left().column).toEqual("A");
});
test("Position.from_name", () => {
const workbook = createWorkbook();
const sheet = createSheet(workbook, "Sheet 1");
sheet.makeCurrent();
sheet.setCell("A", 0, "0");
sheet.focusCell("A", 0);
expect(Position.from_name).toBeDefined();
let position = Position.from_name("A0");
expect(position).toBeInstanceOf(Position);
position = Position.from_name("A123");
expect(position).toBeInstanceOf(Position);
});
});
describe("Range", () => {
test("simple", () => {
const workbook = createWorkbook();
const sheet = createSheet(workbook, "Sheet 1");
sheet.makeCurrent();
sheet.setCell("A", 0, "0");
sheet.setCell("A", 10, "0");
sheet.setCell("B", 1, "0");
sheet.focusCell("A", 0);
expect(R).toBeDefined();
let cellsVisited = 0;
R`A0:A10`.forEach(name => {
++cellsVisited;
});
expect(cellsVisited).toEqual(11);
cellsVisited = 0;
R`A0:A10:1:2`.forEach(name => {
++cellsVisited;
});
expect(cellsVisited).toEqual(6);
});
test("Ranges", () => {
const workbook = createWorkbook();
const sheet = createSheet(workbook, "Sheet 1");
sheet.makeCurrent();
sheet.setCell("A", 0, "0");
sheet.setCell("A", 10, "0");
sheet.setCell("B", 1, "0");
sheet.focusCell("A", 0);
let cellsVisited = 0;
R`A0:A5`.union(R`A6:A10`).forEach(name => {
++cellsVisited;
});
expect(cellsVisited).toEqual(11);
});
});

View file

@ -0,0 +1,166 @@
describe("Basic functions", () => {
const workbook = createWorkbook();
const sheet = createSheet(workbook, "Sheet 1");
sheet.makeCurrent();
sheet.setCell("A", 0, "0");
sheet.setCell("A", 1, "1");
sheet.setCell("A", 2, "2");
test("select", () => {
expect(select).toBeDefined();
expect(select(true, 1, 2)).toBe(1);
expect(select(false, 1, 2)).toBe(2);
});
test("choose", () => {
expect(choose).toBeDefined();
expect(choose(0, 1, 2, 3)).toBe(1);
expect(choose(1, 1, 2, 3)).toBe(2);
expect(choose(3, 1, 2, 3)).toBeUndefined();
expect(choose(-1, 1, 2, 3)).toBeUndefined();
});
test("now", () => {
expect(now).toBeDefined();
expect(now()).toBeInstanceOf(Date);
});
test("randRange", () => {
expect(randRange).toBeDefined();
});
test("integer", () => {
expect(integer).toBeDefined();
expect(integer("0")).toEqual(0);
expect(integer("32")).toEqual(32);
});
test("sheet", () => {
expect(globalThis.sheet).toBeDefined();
expect(globalThis.sheet("Sheet 1")).toBe(sheet);
expect(globalThis.sheet("Not a sheet")).toBeUndefined();
});
test("reduce", () => {
expect(reduce).toBeDefined();
expect(reduce(acc => acc + 1, 0, [1, 2, 3, 4])).toEqual(4);
expect(reduce(acc => acc + 1, 0, [])).toEqual(0);
});
test("numericReduce", () => {
expect(numericReduce).toBeDefined();
expect(numericReduce(acc => acc + 1, 0, [1, 2, 3, 4])).toEqual(4);
expect(numericReduce(acc => acc + 1, 0, [])).toEqual(0);
});
test("numericResolve", () => {
expect(numericResolve).toBeDefined();
expect(numericResolve(["A0", "A1", "A2"])).toEqual([0, 1, 2]);
expect(numericResolve([])).toEqual([]);
});
test("resolve", () => {
expect(resolve).toBeDefined();
expect(resolve(["A0", "A1", "A2"])).toEqual(["0", "1", "2"]);
expect(resolve([])).toEqual([]);
});
});
describe("Statistics", () => {
const workbook = createWorkbook();
const sheet = createSheet(workbook, "Sheet 1");
sheet.makeCurrent();
for (let i = 0; i < 10; ++i) sheet.setCell("A", i, `${i}`);
test("sum", () => {
expect(sum).toBeDefined();
expect(sum(R`A0:A9`)).toEqual(45);
});
test("sumIf", () => {
expect(sumIf).toBeDefined();
expect(sumIf(x => !Number.isNaN(x), R`A0:A10`)).toEqual(45);
});
test("count", () => {
expect(count).toBeDefined();
expect(count(R`A0:A9`)).toEqual(10);
});
test("countIf", () => {
expect(countIf).toBeDefined();
expect(countIf(x => x, R`A0:A10`)).toEqual(10);
});
test("average", () => {
expect(average).toBeDefined();
expect(average(R`A0:A9`)).toEqual(4.5);
});
test("averageIf", () => {
expect(averageIf).toBeDefined();
expect(averageIf(x => !Number.isNaN(x), R`A0:A10`)).toEqual(4.5);
});
test("median", () => {
expect(median).toBeDefined();
expect(median(R`A0:A9`)).toEqual(4.5);
expect(median(R`A0:A2`)).toEqual(1);
});
test("variance", () => {
expect(variance).toBeDefined();
expect(variance(R`A0:A0`)).toEqual(0);
expect(variance(R`A0:A9`)).toEqual(82.5);
});
test("mode", () => {
expect(mode).toBeDefined();
expect(mode(R`A0:A0`.union(R`A0:A0`).union(R`A1:A9`))).toEqual(0);
});
test("stddev", () => {
expect(stddev).toBeDefined();
expect(stddev(R`A0:A0`)).toEqual(0);
expect(stddev(R`A0:A9`)).toEqual(Math.sqrt(82.5));
});
});
describe("Lookup", () => {
const workbook = createWorkbook();
const sheet = createSheet(workbook, "Sheet 1");
sheet.makeCurrent();
for (let i = 0; i < 10; ++i) {
sheet.setCell("A", i, `${i}`);
sheet.setCell("B", i, `B${i}`);
}
sheet.focusCell("A", 0);
test("row", () => {
expect(row()).toEqual(0);
});
test("column", () => {
expect(column()).toEqual("A");
});
test("lookup", () => {
expect(lookup).toBeDefined();
// Note: String ordering.
expect(lookup("2", R`A0:A9`, R`B0:B9`)).toEqual("B2");
expect(lookup("20", R`A0:A9`, R`B0:B9`)).toBeUndefined();
expect(lookup("80", R`A0:A9`, R`B0:B9`, undefined, "nextlargest")).toEqual("B9");
});
test("reflookup", () => {
expect(reflookup).toBeDefined();
// Note: String ordering.
expect(reflookup("2", R`A0:A9`, R`B0:B9`).name).toEqual("B2");
expect(reflookup("20", R`A0:A9`, R`B0:B9`)).toEqual(here());
expect(reflookup("80", R`A0:A9`, R`B0:B9`, undefined, "nextlargest").name).toEqual("B9");
});
});

View file

@ -0,0 +1,192 @@
var thisSheet;
var workbook;
var createWorkbook = () => {
return {
__sheets: new Map(),
sheet(nameOrIndex) {
if (typeof nameOrIndex !== "number") return this.__sheets.get(nameOrIndex);
for (const entry of this.__sheets) {
if (nameOrIndex === 0) return entry[1];
nameOrIndex--;
}
return undefined;
},
};
};
function toBijectiveBase(number) {
const alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
number += 1;
let c = 0;
let x = 1;
while (number >= x) {
++c;
number -= x;
x *= 26;
}
let s = "";
for (let i = 0; i < c; i++) {
s = alpha.charAt(number % 26) + s;
number = Math.floor(number / 26);
}
return s;
}
function __evaluate(expression, that) {
const object = Object.create(null);
for (const entry of that.__cells) {
const cell = JSON.parse(entry[0]);
object[`${cell[0]}${cell[1]}`] = entry[1][1];
}
const sheetObject = that;
let __value;
// Warning: Dragons and fire ahead.
with (that.__workbook) {
with (object) {
with (sheetObject) {
__value = eval(expression);
}
}
}
return __value;
}
class Sheet {
constructor(workbook) {
this.__cells = new Map();
this.__columns = new Set();
this.__workbook = workbook;
this.__currentCellPosition = undefined;
}
get_real_cell_contents(name) {
const cell = this.parse_cell_name(name);
if (cell === undefined) throw new TypeError("Invalid cell name");
return this.getCell(cell.column, cell.row)[0];
}
set_real_cell_contents(name, value) {
const cell = this.parse_cell_name(name);
if (cell === undefined) throw new TypeError("Invalid cell name");
this.setCell(cell.column, cell.row, value);
}
parse_cell_name(name) {
const match = /^([a-zA-Z]+)(\d+)$/.exec(name);
if (!Array.isArray(match)) return undefined;
return {
column: match[1],
row: +match[2],
};
}
current_cell_position() {
return this.__currentCellPosition;
}
column_index(name) {
let i = 0;
for (const column of this.__columns) {
if (column === name) return i;
++i;
}
}
column_arithmetic(name, offset) {
if (offset < 0) {
const columns = this.getColumns();
let index = columns.indexOf(name);
if (index === -1) throw new TypeError(`${name} is not a valid column name`);
index += offset;
if (index < 0) return columns[0];
return columns[index];
}
let found = false;
for (const column of this.__columns) {
if (!found) found = column === name;
if (found) {
if (offset === 0) return column;
offset--;
}
}
if (!found) throw new TypeError(`${name} is not a valid column name`);
let newName;
for (let i = 0; i < offset; ++i) {
newName = toBijectiveBase(this.__columns.size);
this.addColumn(newName);
}
return newName;
}
get_column_bound(name) {
let bound = 0;
for (const entry of this.__cells) {
const [column, row] = JSON.parse(entry[0]);
if (column !== name) continue;
bound = Math.max(bound, row);
}
return row;
}
evaluate(currentColumn, currentRow, expression) {
const currentCellSave = this.__currentCellPosition;
this.__currentCellPosition = { column: currentColumn, row: currentRow };
try {
return __evaluate(expression, this);
} finally {
this.__currentCellPosition = currentCellSave;
}
}
addColumn(name) {
this.__columns.add(name);
}
getColumns() {
return Array.from(this.__columns);
}
setCell(column, row, source, value = undefined) {
this.addColumn(column);
source = `${source}`;
if (value === undefined) {
value = source;
if (value[0] === "=") value = this.evaluate(column, row, value.substr(1));
}
this.__cells.set(JSON.stringify([column, row]), [source, value]);
this[`${column}${row}`] = value;
}
getCell(column, row) {
const data = this.__cells.get(JSON.stringify([column, row]));
if (data === undefined) return undefined;
return data;
}
focusCell(column, row) {
this.__currentCellPosition = { column, row };
}
makeCurrent() {
thisSheet = this;
workbook = this.__workbook;
}
}
var createSheet = (workbook, name) => {
const sheet = new Sheet(workbook);
workbook.__sheets.set(name, sheet);
return sheet;
};

View file

@ -0,0 +1,46 @@
describe("Harness-defined functions", () => {
test("createWorkbook", () => {
expect(createWorkbook).toBeDefined();
const workbook = createWorkbook();
expect(workbook).toBeDefined();
expect(workbook.sheet).toBeDefined();
});
test("createSheet", () => {
const workbook = createWorkbook();
const sheet = createSheet(workbook, "foo");
expect(sheet).toBeDefined();
expect(sheet.get_real_cell_contents).toBeDefined();
expect(sheet.set_real_cell_contents).toBeDefined();
expect(sheet.parse_cell_name).toBeDefined();
expect(sheet.current_cell_position).toBeDefined();
expect(sheet.column_index).toBeDefined();
expect(sheet.column_arithmetic).toBeDefined();
expect(sheet.get_column_bound).toBeDefined();
});
test("Sheet mock behavior", () => {
const workbook = createWorkbook();
const sheet = createSheet(workbook, "foo");
sheet.setCell("A", 0, "10");
expect(sheet.getCell("A", 0)).toEqual(["10", "10"]);
sheet.setCell("A", 0, "=10");
expect(sheet.getCell("A", 0)).toEqual(["=10", 10]);
expect(sheet.getColumns()).toEqual(["A"]);
});
test("Workbook mock behavior", () => {
const workbook = createWorkbook();
const sheet = createSheet(workbook, "foo");
expect(workbook.sheet("foo")).toBe(sheet);
expect(workbook.sheet(0)).toBe(sheet);
expect(workbook.sheet(1)).toBeUndefined();
expect(workbook.sheet("bar")).toBeUndefined();
});
test("Referencing cells", () => {
const workbook = createWorkbook();
const sheet = createSheet(workbook, "foo");
sheet.setCell("A", 0, "42");
sheet.setCell("A", 1, "=A0");
expect(sheet.getCell("A", 1)).toEqual(["=A0", "42"]);
});
});