UNPKG

@platform/cell.typesystem

Version:

The 'strongly typed sheets' system of the CellOS.

1,021 lines 61.9 kB
import { Subject } from 'rxjs'; import { filter } from 'rxjs/operators'; import { TypedSheet } from '.'; import { ERROR, expect, expectError, testInstanceFetch, time, TYPE_DEFS, testFetch, } from '../../test'; import { TypeClient } from '../../TypeSystem.core'; import { TypedSheetData } from './TypedSheetData'; import { TypedSheetRef } from './TypedSheetRef'; import { TypedSheetRefs } from './TypedSheetRefs'; import { TypedSheetRow } from './TypedSheetRow'; import { TypedSheetState } from './TypedSheetState'; describe('TypedSheet', () => { describe('lifecycle', () => { it('dispose', async () => { const { sheet } = await testMySheet(); let fired = 0; sheet.dispose$.subscribe(e => fired++); expect(sheet.isDisposed).to.eql(false); expect(sheet.state.isDisposed).to.eql(false); sheet.dispose(); sheet.dispose(); sheet.dispose(); expect(sheet.isDisposed).to.eql(true); expect(sheet.state.isDisposed).to.eql(true); expect(fired).to.eql(1); }); }); describe('errors', () => { it('error: 404 instance namespace "type.implements" reference not found', async () => { const ns = 'ns:foo.mySheet'; const fetch = await testInstanceFetch({ instance: ns, implements: 'ns:foo.notExist', defs: TYPE_DEFS, rows: [], }); const sheet = await TypedSheet.load({ fetch, ns }); expect(sheet.ok).to.eql(false); expect(sheet.errors.length).to.eql(1); expect(sheet.errors[0].message).to.include(`The namespace (ns:foo.notExist) does not exist`); expect(sheet.errors[0].type).to.eql(ERROR.TYPE.NOT_FOUND); }); it('error: `sheet.data(typename)` requested where typename not part of ns', async () => { const { sheet } = await testMySheet(); const fn = () => sheet.data('NOT_A_TYPENAME'); expect(fn).to.throw(/Definitions for typename 'NOT_A_TYPENAME' not found/); }); }); describe('TypedSheet.info', () => { it('info (sheet exists)', async () => { const { sheet } = await testMySheet(); const info = await sheet.info(); expect(info.exists).to.eql(true); expect(info.ns).to.eql({ type: { implements: 'ns:foo' } }); }); it('info (sheet does not exist)', async () => { const fetch = testFetch({ defs: TYPE_DEFS }); const ns = 'ns:foo.new'; const sheet = await TypedSheet.create({ ns, implements: 'ns:foo', fetch }); const info = await sheet.info(); expect(info.exists).to.eql(false); expect(info.ns).to.eql({}); }); it('info (changed in cache)', async () => { const { sheet } = await testMySheet(); const info1 = await sheet.info(); expect(info1.ns.type).to.eql({ implements: 'ns:foo' }); sheet.state.change.ns({ type: { implements: 'ns:foobar' } }); await time.wait(1); const info2 = await sheet.info(); expect(info2.ns.type).to.eql({ implements: 'ns:foobar' }); }); }); describe('TypedSheet.types', () => { it('single type', async () => { const { sheet } = await testMySheet(); expect(sheet.types.map(type => type.typename)).to.eql(['MyRow']); }); it('multiple types', async () => { const { sheet } = await testMyMultiSheet(); expect(sheet.types.map(type => type.typename)).to.eql(['MyOne', 'MyTwo']); }); it('calculated once (lazy evaluation, shared instance)', async () => { const { sheet } = await testMyMultiSheet(); expect(sheet.types).to.equal(sheet.types); }); it('sheet.implements', async () => { const { sheet } = await testMySheet(); expect(sheet.uri.toString()).to.eql('ns:foo.mySheet'); expect(sheet.implements.toString()).to.eql('ns:foo'); }); }); describe('TypedSheetData (cursor)', () => { it('create: default (unloaded)', async () => { const { sheet } = await testMySheet(); const cursor = sheet.data('MyRow'); expect(cursor.range).to.eql(TypedSheetData.DEFAULT.RANGE); expect(cursor.status).to.eql('INIT'); expect(cursor.total).to.eql(-1); expect(cursor.typename).to.eql('MyRow'); }); it('cursor pooling', async () => { const { sheet } = await testMyMultiSheet(); const cursor1 = sheet.data('MyOne'); const cursor2 = sheet.data('MyOne'); const cursor3 = sheet.data('MyTwo'); expect(cursor1).to.equal(cursor2); expect(cursor1).to.not.equal(cursor3); }); it('cursor pooling: expand range', async () => { const { sheet } = await testMyMultiSheet(); const cursor1 = sheet.data({ typename: 'MyOne', range: '1:10' }); const cursor2 = sheet.data('MyOne'); const cursor3 = sheet.data({ typename: 'MyOne', range: '5:30' }); expect(cursor1).to.equal(cursor2); expect(cursor1).to.equal(cursor3); expect(cursor1.range).to.eql(cursor2.range); expect(cursor1.range).to.eql('5:30'); expect(cursor2.range).to.eql('5:30'); expect(cursor3.range).to.eql('5:30'); }); it('typename/types', async () => { const { sheet } = await testMyMultiSheet(); const cursor1 = sheet.data('MyOne'); const cursor2 = sheet.data('MyTwo'); expect(cursor1.typename).to.eql('MyOne'); expect(cursor1.types.map(def => def.prop)).to.eql(['title', 'foo']); expect(cursor1.types.map(def => def.column)).to.eql(['A', 'B']); expect(cursor2.typename).to.eql('MyTwo'); expect(cursor2.types.map(def => def.prop)).to.eql(['bar', 'name']); expect(cursor2.types.map(def => def.column)).to.eql(['B', 'C']); }); it('create: custom range (auto correct)', async () => { const DEFAULT = TypedSheetData.DEFAULT; const test = async (range, expected) => { const { sheet } = await testMySheet(); const res = sheet.data({ typename: 'MyRow', range }); expect(res.range).to.eql(expected || range); }; await test('3:15'); await test('10:50'); await test('1:80'); await test('', DEFAULT.RANGE); await test(' ', DEFAULT.RANGE); await test('0:0', '1:1'); await test('0:10', '1:10'); await test('10:0', '1:10'); await test('500:500'); await test('.:.', DEFAULT.RANGE); await test('-1:10', DEFAULT.RANGE); await test('1:-10', DEFAULT.RANGE); await test('A:5', '1:5'); await test('C:5', '1:5'); await test('5:C', '1:5'); await test('*:*', DEFAULT.RANGE); await test('**:**', DEFAULT.RANGE); await test('*:**', DEFAULT.RANGE); await test('**:*', DEFAULT.RANGE); await test('1:*', DEFAULT.RANGE); await test('1:**', DEFAULT.RANGE); await test('*:1', DEFAULT.RANGE); await test('**:1', DEFAULT.RANGE); await test('0:*', `1:${DEFAULT.PAGE}`); await test('10:*', `10:${DEFAULT.PAGE}`); await test('*:800', `${DEFAULT.PAGE}:800`); await test('800:*', `${DEFAULT.PAGE}:800`); }); it('load (status: INIT ➔ LOADING ➔ LOADED)', async () => { const { sheet } = await testMySheet(); const cursor = sheet.data('MyRow'); expect(cursor.isLoaded).to.eql(false); expect(cursor.status).to.eql('INIT'); expect(cursor.total).to.eql(-1); expect(cursor.typename).to.eql('MyRow'); expect(cursor.row(0).isLoaded).to.eql(false); const wait = cursor.load(); expect(cursor.status).to.eql('LOADING'); expect(cursor.total).to.eql(-1); await wait; expect(cursor.status).to.eql('LOADED'); expect(cursor.isLoaded).to.eql(true); expect(cursor.total).to.eql(9); expect(cursor.row(0).isLoaded).to.eql(true); expect(cursor.row(8).isLoaded).to.eql(true); expect(cursor.row(9).isLoaded).to.eql(false); }); it('load (subset)', async () => { const { sheet } = await testMySheet(); const cursor = await sheet.data({ typename: 'MyRow', range: '2:5' }).load(); expect(cursor.row(0).isLoaded).to.eql(false); expect(cursor.row(1).isLoaded).to.eql(true); expect(cursor.row(4).isLoaded).to.eql(true); expect(cursor.row(5).isLoaded).to.eql(false); }); it('load (expand range from [loaded] state)', async () => { const { sheet } = await testMySheet(); const cursor = await sheet.data({ typename: 'MyRow', range: '1:5' }).load(); expect(cursor.isLoaded).to.eql(true); expect(cursor.range).to.eql('1:5'); expect(cursor.row(0).isLoaded).to.eql(true); expect(cursor.row(8).isLoaded).to.eql(false); await cursor.load('3:15'); expect(cursor.range).to.eql('1:15'); expect(cursor.row(8).isLoaded).to.eql(true); expect(cursor.row(14).isLoaded).to.eql(false); }); it('load (reset range from [unloaded] state)', async () => { const { sheet } = await testMySheet(); const cursor = sheet.data('MyRow'); expect(cursor.isLoaded).to.eql(false); expect(cursor.range).to.eql(TypedSheetData.DEFAULT.RANGE); await cursor.load('3:15'); expect(cursor.isLoaded).to.eql(true); expect(cursor.range).to.eql('3:15'); expect(cursor.row(0).isLoaded).to.eql(false); expect(cursor.row(1).isLoaded).to.eql(false); expect(cursor.row(2).isLoaded).to.eql(true); expect(cursor.row(8).isLoaded).to.eql(true); expect(cursor.row(14).isLoaded).to.eql(false); }); it('load (multiple types)', async () => { const { sheet } = await testMyMultiSheet(); const cursor1 = sheet.data('MyOne'); const cursor2 = sheet.data('MyTwo'); expect(cursor1.row(0).props.foo).to.eql('foo-default'); expect(cursor2.row(9).props.bar).to.eql('bar-default'); }); it('events: loading | loaded', async () => { const { sheet } = await testMySheet(); const cursor = sheet.data('MyRow'); const fired = []; sheet.event$ .pipe(filter(e => e.type === 'SHEET/loading' || e.type === 'SHEET/loaded')) .subscribe(e => fired.push(e)); await cursor.load(); expect(fired.length).to.eql(2); const e1 = fired[0]; const e2 = fired[1]; expect(e1.type).to.eql('SHEET/loading'); expect(e1.payload.sheet).to.equal(sheet); expect(e1.payload.range).to.eql(cursor.range); expect(e2.type).to.eql('SHEET/loaded'); expect(e2.payload.sheet).to.equal(sheet); expect(e2.payload.range).to.eql(cursor.range); expect(e2.payload.total).to.eql(9); }); it('does not load twice if already LOADING', async () => { const { sheet } = await testMySheet(); const cursor = sheet.data('MyRow'); const fired = []; sheet.event$ .pipe(filter(e => e.type === 'SHEET/loading' || e.type === 'SHEET/loaded')) .subscribe(e => fired.push(e)); await Promise.all([cursor.load(), cursor.load(), cursor.load()]); expect(fired.length).to.eql(2); }); it('does load twice if query differs', async () => { const { sheet } = await testMySheet(); const cursor = sheet.data('MyRow'); const fired = []; sheet.event$ .pipe(filter(e => e.type === 'SHEET/loading' || e.type === 'SHEET/loaded')) .subscribe(e => fired.push(e)); await Promise.all([ cursor.load(), cursor.load(), cursor.load('4:6'), cursor.load(), cursor.load('4:6'), cursor.load(), ]); expect(fired.length).to.eql(4); }); it('throw: row out-of-bounds (index: -1)', async () => { const { sheet } = await testMySheet(); const cursor = await sheet.data('MyRow').load(); const err = /Row index must be >=0/; expect(() => cursor.row(-1)).to.throw(err); }); it('exists', async () => { const { sheet } = await testMySheet(); const cursor = await sheet.data('MyRow').load(); expect(cursor.exists(-1)).to.eql(false); expect(cursor.exists(0)).to.eql(true); expect(cursor.exists(99)).to.eql(false); }); it('retrieves non-existent row', async () => { const { sheet } = await testMySheet(); const cursor = await sheet.data('MyRow').load(); expect(cursor.exists(99)).to.eql(false); expect(cursor.row(99)).to.not.eql(undefined); }); it('toObject', async () => { const { sheet } = await testMyEnumSheet(); const row = (await sheet.data('Enum').load()).row(0); expect(row.toObject()).to.eql({ single: 'hello', union: ['blue'], array: ['red', 'green', 'blue'], }); }); describe('functional methods', () => { it('forEach', async () => { const { sheet } = await testMySheet(); const cursor = await sheet.data('MyRow').load(); const indexes = []; const rows = []; cursor.forEach((row, i) => { indexes.push(i); rows.push(row); }); expect(indexes).to.eql([0, 1, 2, 3, 4, 5, 6, 7, 8]); expect(rows.length).to.eql(9); expect(rows[0].title).to.eql('One'); expect(rows[8].title).to.eql('Nine'); }); it('map', async () => { const { sheet } = await testMySheet(); const cursor = sheet.data('MyRow'); const res1 = cursor.map(row => row.title); expect(res1).to.eql([]); await cursor.load(); const res2 = cursor.map(row => row.title); expect(res2).to.eql([ 'One', 'Two', 'Untitled', 'Untitled', 'Untitled', 'Untitled', 'Untitled', 'Untitled', 'Nine', ]); const indexes = []; cursor.map((row, i) => indexes.push(i)); expect(indexes).to.eql([0, 1, 2, 3, 4, 5, 6, 7, 8]); }); it('filter', async () => { const { sheet } = await testMySheet(); const cursor = await sheet.data('MyRow').load(); const res = cursor.filter(r => r.title.endsWith('e')).map(r => r.props.title); expect(res).to.eql(['One', 'Nine']); const indexes = []; cursor.filter((row, i) => { indexes.push(i); return true; }); expect(indexes).to.eql([0, 1, 2, 3, 4, 5, 6, 7, 8]); }); it('find', async () => { var _a; const { sheet } = await testMySheet(); const cursor = await sheet.data('MyRow').load(); const res1 = cursor.find(row => row.title === 'Nine'); const res2 = cursor.find(row => row.title === '404'); expect((_a = res1) === null || _a === void 0 ? void 0 : _a.props.title).to.eql('Nine'); expect(res2).to.eql(undefined); const indexes = []; cursor.map((row, i) => { indexes.push(i); return false; }); expect(indexes).to.eql([0, 1, 2, 3, 4, 5, 6, 7, 8]); }); }); }); describe('TypedSheetRow', () => { const testRow = async (uri) => { const { sheet, fetch } = await testMySheet(); const ctx = TypedSheet.ctx({ fetch }); const ns = await TypeClient.load({ ns: 'ns:foo', fetch: ctx.fetch, cache: ctx.cache }); const defs = ns.defs; const columns = ns.defs[0].columns; const typename = 'MyRow'; const row = TypedSheetRow.create({ typename, uri, columns, ctx, sheet }); return { row, ctx, defs, sheet }; }; it('throw: URI not a row', async () => { expectError(async () => testRow('cell:foo:A1')); expectError(async () => testRow('ns:foo')); expectError(async () => testRow('file:foo:abc')); }); it('create (not loaded)', async () => { const { row, defs } = await testRow('cell:foo:1'); expect(row.uri.toString()).to.eql('cell:foo:1'); expect(row.typename).to.eql('MyRow'); expect(row.index).to.eql(0); expect(row.status).to.eql('INIT'); expect(row.isLoaded).to.eql(false); expect(row.types.list[0].column).to.eql(defs[0].columns[0].column); expect(row.types.map.title.column).to.eql('A'); expect(row.props.title).to.eql('Untitled'); expect(row.props.isEnabled).to.eql(undefined); }); it('load', async () => { const { row } = await testRow('cell:foo:1'); expect(row.typename).to.eql('MyRow'); expect(row.props.title).to.eql('Untitled'); expect(row.props.isEnabled).to.eql(undefined); expect(row.isLoaded).to.eql(false); expect(row.status).to.eql('INIT'); const res = row.load(); expect(row.isLoaded).to.eql(false); expect(row.status).to.eql('LOADING'); await res; expect(row.isLoaded).to.eql(true); expect(row.status).to.eql('LOADED'); expect(row.props.title).to.eql('One'); expect(row.props.isEnabled).to.eql(true); }); it('load (static)', async () => { const { defs, sheet, ctx } = await testRow('cell:foo:1'); const uri = 'cell:foo:1'; const columns = defs[0].columns; const typename = 'MyRow'; const row = await TypedSheetRow.load({ sheet, typename, uri, columns, ctx }); expect(row.props.title).to.eql('One'); expect(row.props.isEnabled).to.eql(true); }); it('load (subset of props)', async () => { const { row } = await testRow('cell:foo:1'); expect(row.props.title).to.eql('Untitled'); expect(row.props.isEnabled).to.eql(undefined); await row.load({ props: ['title'] }); expect(row.props.title).to.eql('One'); expect(row.props.isEnabled).to.eql(undefined); }); it('updates when prop changed elsewhere via event (ie. change not via row instance API)', async () => { const { row, ctx } = await testRow('cell:foo:1'); expect(row.props.title).to.eql('Untitled'); await row.load(); expect(row.props.title).to.eql('One'); ctx.event$.next({ type: 'SHEET/change', payload: { kind: 'CELL', ns: 'ns:foo', key: 'A1', to: { value: 'Hello!' }, }, }); expect(row.props.title).to.eql('Hello!'); row.props.title = 'Foobar'; expect(row.props.title).to.eql('Foobar'); }); describe('row event$', () => { it('fires load events', async () => { const { sheet } = await testMySheet(); const cursor = sheet.data('MyRow'); const fired = []; sheet.event$ .pipe(filter(e => e.type.startsWith('SHEET/row/load'))) .subscribe(e => fired.push(e)); await cursor.load(); const loading = fired .filter(e => e.type === 'SHEET/row/loading') .map(e => e.payload); const loaded = fired .filter(e => e.type === 'SHEET/row/loaded') .map(e => e.payload); expect(loading.length).to.eql(9); expect(loaded.length).to.eql(9); expect(loading.every(e => e.sheet === sheet)).to.eql(true); expect(loaded.every(e => e.sheet === sheet)).to.eql(true); }); it('repeat loads', async () => { const { sheet } = await testMySheet(); const cursor = sheet.data('MyRow'); const fired = []; sheet.event$ .pipe(filter(e => e.type === 'SHEET/row/loading' || e.type === 'SHEET/row/loaded')) .subscribe(e => fired.push(e)); await cursor.load(); expect(fired.length).to.eql(18); await cursor.load(); expect(fired.length).to.eql(18); const row = cursor.row(0); await row.load(); expect(fired.length).to.eql(18); await row.load({ force: true }); expect(fired.length).to.eql(18 + 2); }); it('shared load promise', async () => { const { sheet } = await testMySheet(); const cursor = sheet.data('MyRow'); const row = cursor.row(0); const fired = []; sheet.event$ .pipe(filter(e => e.type === 'SHEET/row/loading' || e.type === 'SHEET/row/loaded')) .subscribe(e => fired.push(e)); await Promise.all([ row.load(), row.load({ props: ['title'] }), row.load(), row.load(), row.load(), row.load(), row.load({ props: ['title'] }), ]); expect(fired.length).to.eql(4); await row.load({ force: true }); expect(fired.length).to.eql(6); await row.load({ force: false }); expect(fired.length).to.eql(6); }); it('fires on row property set (only when changed)', async () => { const { sheet } = await testMySheet(); const cursor = sheet.data('MyRow'); const row = cursor.row(0); await row.load(); const fired = []; sheet.event$.subscribe(e => fired.push(e)); row.props.title = 'Foo'; expect(fired.length).to.eql(1); expect(fired[0].type).to.eql('SHEET/change'); row.props.title = 'Foo'; expect(fired.length).to.eql(1); }); }); describe('row types', () => { it('row.types.list', async () => { const { sheet } = await testMySheet(); const cursor = await sheet.data('MyRow').load(); const row = cursor.row(0); const types = row.types; const list1 = types.list; const list2 = types.list; expect(list1).to.equal(list2); expect(list1.map(def => def.column)).to.eql(['A', 'B', 'C', 'D', 'E']); }); it('row.types.map', async () => { const { sheet } = await testMySheet(); const cursor = await sheet.data('MyRow').load(); const row = cursor.row(0); const types = row.types.map; expect(types.title.column).to.eql('A'); expect(types.isEnabled.column).to.eql('B'); expect(types.color.column).to.eql('C'); expect(types.message.column).to.eql('D'); expect(types.messages.column).to.eql('E'); }); it('row.types (uri)', async () => { const { sheet } = await testMySheet(); const cursor = await sheet.data('MyRow').load(); const types = cursor.row(0).types; expect(types.map.title.uri.toString()).to.eql('cell:foo.mySheet:A1'); expect(types.map.messages.uri.toString()).to.eql('cell:foo.mySheet:E1'); types.list.forEach(item => { const uri = types.map[item.prop].uri; expect(uri).to.equal(item.uri); }); }); }); describe('default value', () => { it('simple: primitive | {object}', async () => { const { sheet } = await testMyPrimitivesSheet(); const cursor = await sheet.data('Primitives').load(); const row1 = cursor.row(0).props; const row2 = cursor.row(99).props; expect(row1.stringValue).to.eql('hello value'); expect(row2.stringValue).to.eql('Hello (Default)'); }); it('ref (look up cell address)', async () => { const ns = 'ns:foo.sample'; const fetch = await testInstanceFetch({ instance: ns, implements: 'ns:foo.defaults', defs: TYPE_DEFS, rows: [], cells: { A1: { value: 'my-foo-default' } }, }); const sheet = await TypedSheet.load({ fetch, ns }); const cursor = await sheet.data('MyDefault').load(); expect(cursor.exists(99)).to.eql(false); }); }); describe('row.prop (get/set methods)', () => { it('reuse api instance', async () => { const { sheet } = await testMyPrimitivesSheet(); const row = (await sheet.data('Primitives').load()).row(0); const prop1 = row.prop('numberProp'); const prop2 = row.prop('numberProp'); const prop3 = row.prop('stringValue'); expect(prop1).to.equal(prop2); expect(prop1).to.not.equal(prop3); }); it('get', async () => { const { sheet } = await testMyPrimitivesSheet(); const cursor = await sheet.data('Primitives').load(); const prop1 = cursor.row(0).prop('stringValue'); const prop2 = cursor.row(99).prop('stringValue'); expect(prop1.get()).to.eql('hello value'); expect(prop2.get()).to.eql('Hello (Default)'); }); it('set', async () => { const { sheet } = await testMyPrimitivesSheet(); const cursor = await sheet.data('Primitives').load(); const prop = cursor.row(0).prop('stringValue'); const state = sheet.state; prop.set(''); expect(prop.get()).to.eql(''); expect(await state.getCell('A1')).to.eql({ value: 'hello value' }); await time.wait(1); expect(await state.getCell('A1')).to.eql({ value: '' }); prop.set(' '); expect(prop.get()).to.eql(' '); prop.set('foo'); expect(prop.get()).to.eql('foo'); }); it('set: throw if attempt to set ref', async () => { const { sheet } = await testMySheet(); const cursor = await sheet.data('MyRow').load(); const row = cursor.row(0); expect(() => row.prop('messages').set({})).to.throw(/Cannot write to property/); expect(() => row.prop('message').set({})).to.throw(/Cannot write to property/); row.prop('message').clear(); row.prop('messages').clear(); }); it('clear', async () => { const { sheet } = await testMyPrimitivesSheet(); const cursor = await sheet.data('Primitives').load(); const row = cursor.row(0); const prop = row.prop('stringValue'); expect(prop.get()).to.eql('hello value'); prop.clear(); expect(prop.get()).to.eql('Hello (Default)'); }); }); describe('read/write (inline)', () => { it('{ object }', async () => { const { sheet } = await testMySheet(); const cursor = await sheet.data('MyRow').load(); const row = cursor.row(0).props; expect(row.title).to.eql('One'); expect(row.color).to.eql({ label: 'background', color: 'red' }); expect(row.isEnabled).to.eql(true); row.title = 'hello'; row.color = { label: 'background', color: 'green', description: 'Yo' }; expect(row.title).to.eql('hello'); expect(row.color).to.eql({ label: 'background', color: 'green', description: 'Yo', }); row.title = ''; row.color = undefined; expect(row.title).to.eql(''); expect(row.color).to.eql(undefined); }); describe('enum', () => { it('single', async () => { const { sheet } = await testMyEnumSheet(); const cursor = await sheet.data('Enum').load(); const row = cursor.row(0).props; expect(row.single).to.eql('hello'); row.single = undefined; expect(row.single).to.eql(undefined); }); it('union', async () => { const { sheet } = await testMyEnumSheet(); const cursor = await sheet.data('Enum').load(); const row = cursor.row(0).props; expect(row.union).to.eql(['blue']); row.union = 'red'; expect(row.union).to.eql('red'); row.union = ['blue', 'blue']; expect(row.union).to.eql(['blue', 'blue']); row.union = undefined; expect(row.union).to.eql(undefined); }); it('array', async () => { const { sheet } = await testMyEnumSheet(); const cursor = await sheet.data('Enum').load(); const row = cursor.row(0).props; expect(row.array).to.eql(['red', 'green', 'blue']); row.array = undefined; expect(row.array).to.eql([]); }); }); describe('primitive', () => { it('string', async () => { const { sheet } = await testMyPrimitivesSheet(); const cursor = await sheet.data('Primitives').load(); const row = cursor.row(0).props; expect(row.stringValue).to.eql('hello value'); expect(row.stringProp).to.eql('hello prop'); row.stringValue = ''; row.stringProp = ''; expect(row.stringValue).to.eql(''); expect(row.stringProp).to.eql(''); row.stringValue = 'Foo'; row.stringProp = 'Bar'; expect(row.stringValue).to.eql('Foo'); expect(row.stringProp).to.eql('Bar'); }); it('number', async () => { const { sheet } = await testMyPrimitivesSheet(); const cursor = await sheet.data('Primitives').load(); const row = cursor.row(0).props; expect(row.numberValue).to.eql(123); expect(row.numberProp).to.eql(456); row.numberValue = -1; row.numberProp = -1; expect(row.numberValue).to.eql(-1); expect(row.numberProp).to.eql(-1); }); it('boolean', async () => { const { sheet } = await testMyPrimitivesSheet(); const cursor = await sheet.data('Primitives').load(); const row = cursor.row(0).props; expect(row.booleanValue).to.eql(true); expect(row.booleanProp).to.eql(true); row.booleanValue = false; row.booleanProp = false; expect(row.booleanValue).to.eql(false); expect(row.booleanProp).to.eql(false); }); it('null', async () => { const { sheet } = await testMyPrimitivesSheet(); const cursor = await sheet.data('Primitives').load(); const row = cursor.row(0).props; expect(row.nullValue).to.eql(null); row.nullValue = 123; row.nullProp = 123; expect(row.nullValue).to.eql(123); expect(row.nullProp).to.eql(123); row.nullValue = null; row.nullProp = null; expect(row.nullValue).to.eql(null); expect(row.nullProp).to.eql(null); }); it('undefined', async () => { const { sheet } = await testMyPrimitivesSheet(); const cursor = await sheet.data('Primitives').load(); const row = cursor.row(0).props; expect(row.undefinedValue).to.eql(undefined); expect(row.undefinedProp).to.eql(undefined); row.undefinedValue = 'hello'; row.undefinedProp = 'hello'; expect(row.undefinedValue).to.eql('hello'); expect(row.undefinedProp).to.eql('hello'); row.undefinedValue = undefined; row.undefinedProp = undefined; expect(row.undefinedValue).to.eql(undefined); expect(row.undefinedProp).to.eql(undefined); }); }); }); describe('read/write (ref)', () => { describe('1:1 (TypedSheetRefs)', () => { it('single row', async () => { var _a; const { sheet } = await testMySheet(); const cursor = await sheet.data('MyRow').load(); const row = cursor.row(0); const message = row.props.message; expect(message).to.be.an.instanceof(TypedSheetRef); expect((_a = message) === null || _a === void 0 ? void 0 : _a.typename).to.eql('MyMessage | null'); expect(message).to.equal(row.props.message); }); }); describe('1:* (TypedSheetRefs)', () => { it('caches property instance of [TypedSheetRefs]', async () => { const { sheet } = await testMySheet(); const cursor = await sheet.data('MyRow').load(); const row = cursor.row(0); const props = row.props; expect(props.messages).to.be.an.instanceof(TypedSheetRefs); expect(props.messages).to.equal(props.messages); expect(props.messages === props.messages).to.eql(true); expect((await props.messages.data()) === (await props.messages.data())).to.eql(true); expect((await props.messages.load()) === (await props.messages.load())).to.eql(true); }); it('load ➔ ready (loaded)', async () => { var _a; const { sheet } = await testMySheet(); const cursor = await sheet.data('MyRow').load(); const row = cursor.row(0); const messages = row.props.messages; expect(messages.isLoaded).to.eql(false); expect(messages.typename).to.eql('MyMessage'); const type = messages.typeDef.type; expect(type.kind).to.eql('REF'); if (type.kind === 'REF') { expect(type.types[0].default).to.eql({ value: -1 }); expect(type.types[1].default).to.eql({ value: 'anon' }); expect(type.types[2].default).to.eql(undefined); } await messages.load(); expect(messages.isLoaded).to.eql(true); expect(messages.sheet).to.be.an.instanceof(TypedSheet); expect(messages.ns.toString()).to.eql(messages.sheet.uri.toString()); const childCursor = await messages.data({ range: '1:10' }); const childRow = childCursor.row(0); const childRowProps = childRow.props; expect(childRow.types.list.map(item => item.type)).to.eql(messages.sheet.types[0].columns.map(item => item.type)); expect(childRowProps.message).to.eql(undefined); childRowProps.message = 'hello'; childRowProps.user = 'bob'; expect(childRowProps.message).to.eql('hello'); expect(childRowProps.user).to.eql('bob'); const changes = sheet.state.changes; const changedLinks = ((_a = changes.cells) === null || _a === void 0 ? void 0 : _a.E1.to.links) || {}; expect(changedLinks['ref:type']).to.eql(messages.sheet.uri.toString()); }); it('fires event: SHEET/refs/(loading | loaded)', async () => { const { sheet } = await testMySheet(); const fired = []; sheet.event$ .pipe(filter(e => e.type.startsWith('SHEET/refs/load'))) .subscribe(e => fired.push(e)); const row = (await sheet.data('MyRow').load()).row(0).props; const messages = row.messages; expect(fired.length).to.eql(0); await messages.load(); expect(fired.length).to.eql(2); expect(fired[0].type).to.eql('SHEET/refs/loading'); expect(fired[1].type).to.eql('SHEET/refs/loaded'); const loading = fired[0].payload; const loaded = fired[1].payload; expect(loading.sheet).to.equal(sheet); expect(loaded.sheet).to.equal(sheet); expect(loading.refs).to.equal(messages); expect(loaded.refs).to.equal(messages); expect(loading.sheet).to.equal(sheet); expect(loaded.sheet).to.equal(sheet); await messages.load(); await messages.load(); await messages.load(); expect(fired.length).to.eql(2); }); it('throw: sheet called before ready (loaded)', async () => { const { sheet } = await testMySheet(); const row = (await sheet.data('MyRow').load()).row(0).props; const fn = () => row.messages.sheet; expect(fn).to.throw(/called before ready \[isLoaded\]/); }); it('load called only once', async () => { const { sheet } = await testMySheet(); const row = (await sheet.data('MyRow').load()).row(0).props; const messages = row.messages; await Promise.all([messages.load(), messages.load(), messages.load()]); expect(messages.isLoaded).to.eql(true); }); it('has placeholder URI prior to being ready (loaded)', async () => { const { sheet } = await testMySheet(); const cursor = await sheet.data('MyRow').load(); const messages = cursor.row(0).props.messages; expect(messages.isLoaded).to.eql(false); expect(messages.ns.toString()).to.eql(TypedSheetRefs.PLACEHOLDER); await messages.load(); expect(messages.ns.toString()).to.not.eql(TypedSheetRefs.PLACEHOLDER); }); it('uses existing link', async () => { const { sheet } = await testMySheet(); const cursorA = await sheet.data({ typename: 'MyRow', range: '1:3' }).load(); const cursorB = await sheet.data({ typename: 'MyRow', range: '1:10' }).load(); const rowA = cursorA.row(0).props; await rowA.messages.load(); const rowB = cursorB.row(0).props; await rowB.messages.load(); expect(rowA.messages.ns.toString()).to.not.eql(TypedSheetRefs.PLACEHOLDER); expect(rowB.messages.ns.toString()).to.not.eql(TypedSheetRefs.PLACEHOLDER); expect(rowA.messages.ns.toString()).to.eql(rowB.messages.ns.toString()); }); it('ref.data(...): auto loads (await load)', async () => { const { sheet } = await testMySheet(); const row = sheet.data('MyRow').row(0).props; const cursor1 = await row.messages.data({ range: '1:5' }); const cursor2 = await row.messages.data(); const cursor3 = await row.messages.data({ range: '1:10' }); expect(cursor1).to.equal(cursor2); expect(cursor1).to.equal(cursor3); expect(cursor1.typename).to.eql('MyMessage'); expect(cursor1.isLoaded).to.eql(true); expect(cursor1.status).to.eql('LOADED'); expect(cursor1.range).to.eql('1:10'); }); it('ref.data(...): loaded props', async () => { const { sheet } = await testMySheet(); const row = sheet.data('MyRow').row(0).props; const cursor = await row.messages.data(); const childRow = cursor.row(0).props; childRow.message = 'hello'; childRow.user = 'bob'; expect(childRow.message).to.eql('hello'); expect(childRow.user).to.eql('bob'); }); }); }); }); describe('TypedSheetState', () => { it('exposed from sheet', async () => { const { sheet } = await testMySheet(); const state = sheet.state; expect(state.uri).to.eql(sheet.uri); expect(state).to.be.an.instanceof(TypedSheetState); }); describe('internal: getCell', () => { it('not found', async () => { const { sheet } = await testMySheet(); const state = sheet.state; const res = await state.getCell('ZZ99'); expect(res).to.eql(undefined); }); it('retrieve from fetch (then cache)', async () => { const { sheet, fetch } = await testMySheet(); const state = sheet.state; expect(fetch.getCellsCount).to.eql(0); const res = await state.getCell('A1'); expect(res).to.eql({ value: 'One' }); expect(fetch.getCellsCount).to.eql(1); await state.getCell('A1'); expect(fetch.getCellsCount).to.eql(1); }); it('throw: invalid key', async () => { const { sheet } = await testMySheet(); const state = sheet.state; expectError(async () => state.getCell('A'), 'Expected a cell key (eg "A1")'); }); describe('internal: getNs', () => { it('retrieve from fetch (then cache)', async () => { const { sheet, fetch } = await testMySheet(); const state = sheet.state; fetch.getNsCount = 0; const res = await state.getNs(); expect(res).to.eql(undefined); expect(fetch.getNsCount).to.eql(1); await state.getNs(); expect(fetch.getNsCount).to.eql(1); }); }); }); describe('ignores (no change)', () => { it('ignores different namespace', async () => { const { sheet, event$ } = await testMySheet(); const state = sheet.state; expect(state.changes).to.eql({}); event$.next({ type: 'SHEET/change', payload: { kind: 'CELL', ns: 'ns:foo.BAR', key: 'A1', to: { value: 123 } }, }); event$.next({ type: 'SHEET/change', payload: { kind: 'CELL', ns: 'foo.BAR', key: 'A1', to: { value: 123 } }, }); await time.wait(1); expect(state.changes).to.eql({}); }); it('ignores non cell URIs', async () => { const { sheet, event$ } = await testMySheet(); const state = sheet.state; expect(state.changes).to.eql({}); event$.next({ type: 'SHEET/change', payload: { kind: 'CELL', ns: 'file:foo:abc', key: 'A1', to: { value: 123 } }, }); await time.wait(1); expect(state.changes).to.eql({}); }); it('ignores invalid URIs', async () => { const { sheet, event$ } = await testMySheet(); const state = sheet.state; expect(state.changes).to.eql({}); event$.next({ type: 'SHEET/change', payload: { kind: 'CELL', ns: 'ns:not-valid*', key: 'A1', to: { value: 123 } }, }); event$.next({ type: 'SHEET/change', payload: { kind: 'CELL', ns: 'not-valid*', key: 'A1', to: { value: 123 } }, }); event$.next({ type: 'SHEET/change', payload: { kind: 'CELL', ns: 'ns:foo', key: 'A-1', to: { value: 123 } }, }); event$.next({ type: 'SHEET/change', payload: { kind: 'CELL', ns: 'foo', key: 'A-1', to: { value: 123 } }, }); event$.next({ type: 'SHEET/change', payload: { kind: 'CELL', ns: 'ns:foo:A1', key: 'A1', to: { value: 123 } }, }); event$.next({ type: 'SHEET/change', payload: { kind: 'CELL', ns: 'foo:A1', key: 'A1', to: { value: 123 } }, }); await time.wait(1); expect(state.changes).to.eql({}); }); it('disposed: no change', async () => { const { sheet, event$ } = await testMySheet(); expect(sheet.state.changes).to.eql({}); sheet.dispose(); event$.next({ type: 'SHEET/change', payload: { kind: 'CELL', ns: 'foo.mySheet', key: 'A1', to: { value: 123 } }, }); await time.wait(1); expect(sheet.state.changes).to.eql({}); }); }); describe('changes', () => { it('hasChanges: cell', async () => { const { sheet } = await testMySheet(); const state = sheet.state; expect(state.hasChanges).to.eql(false); state.change.cell('A1', { value: 123 }); await time.wait(1); expect(state.hasChanges).to.eql(true); }); it('hasChanges: ns', async () => { const { sheet } = await testMySheet(); const state = sheet.state; expect(state.hasChanges).to.eql(false); state.change.ns({ type: { implements: 'foobar' } }); await time.wait(1); expect(state.hasChanges).to.eql(true); }); it('state.c