// switcher.mjs's rule: in the phone view an edit to a block's box, // size, order or hidden becomes set_phone over the whole override map; // on the desktop, box and size are the block's own. Plus the stored // view, read defensively. import test from 'node:test' import assert from 'node:assert/strict' import * as Y from 'yjs' import { applyOps, view } from './ops.mjs' import { routeEdit, phoneMap, resetPhone, hasOverride, readView, writeView, STORAGE_KEY } from './switcher.mjs' const block = { id: 'blockaaaaaaa', type: 'text', props: { size: 'm' }, box: { x: 0.1, y: 0.1, w: 0.5, h: 0.3 }, z: 0, phone: {} } const withPhone = { ...block, phone: { hidden: true, order: 2, size: 's', box: { x: 0, y: 0, w: 1, h: 0.5 } } } test('desktop view: box and size are the block; hidden and order are always the phone map', () => { assert.deepEqual(routeEdit({ view: 'desktop', slide: 's', block, field: 'box', value: { x: 0.2, y: 0.2, w: 0.5, h: 0.3 } }), { op: 'set_block', slide: 's', id: block.id, box: { x: 0.2, y: 0.2, w: 0.5, h: 0.3 } }) assert.deepEqual(routeEdit({ view: 'desktop', slide: 's', block, field: 'size', value: 'l' }), { op: 'set_block', slide: 's', id: block.id, props: { size: 'l' } }) assert.deepEqual(routeEdit({ view: 'desktop', slide: 's', block, field: 'size', value: '' }), { op: 'set_block', slide: 's', id: block.id, props: { size: null } }) assert.deepEqual(routeEdit({ view: 'desktop', slide: 's', block, field: 'hidden', value: true }), { op: 'set_phone', slide: 's', id: block.id, phone: { hidden: true } }) assert.deepEqual(routeEdit({ view: 'both', slide: 's', block, field: 'order', value: 3 }), { op: 'set_phone', slide: 's', id: block.id, phone: { order: 3 } }) }) test('phone view: everything goes to set_phone, carrying the rest of the map', () => { assert.deepEqual(routeEdit({ view: 'phone', slide: 's', block: withPhone, field: 'box', value: { x: 0.12345, y: 0, w: 0.5, h: 0.5 } }), { op: 'set_phone', slide: 's', id: block.id, phone: { hidden: true, order: 2, size: 's', box: { x: 0.1235, y: 0, w: 0.5, h: 0.5 } } }) assert.deepEqual(routeEdit({ view: 'phone', slide: 's', block: withPhone, field: 'size', value: 'l' }).phone.size, 'l') assert.deepEqual(routeEdit({ view: 'phone', slide: 's', block: withPhone, field: 'hidden', value: false }).phone, { order: 2, size: 's', box: { x: 0, y: 0, w: 1, h: 0.5 } }) assert.deepEqual(routeEdit({ view: 'phone', slide: 's', block: withPhone, field: 'order', value: null }).phone, { hidden: true, size: 's', box: { x: 0, y: 0, w: 1, h: 0.5 } }) assert.deepEqual(routeEdit({ view: 'phone', slide: 's', block, field: 'size', value: 'l' }), { op: 'set_phone', slide: 's', id: block.id, phone: { size: 'l' } }) assert.throws(() => routeEdit({ view: 'phone', slide: 's', block, field: 'level', value: 2 })) }) test('the ops it makes are ones the document takes', () => { const doc = new Y.Doc() applyOps(doc, [ { op: 'add_slide', id: 'slideaaaaaaa', layout: 'canvas' }, { op: 'add_block', slide: 'slideaaaaaaa', id: block.id, type: 'text', box: block.box, z: 0, markdown: 'x' } ], 'test') const b = view(doc).slides[0].blocks[0] applyOps(doc, [routeEdit({ view: 'phone', slide: 'slideaaaaaaa', block: b, field: 'box', value: { x: 0, y: 0, w: 1, h: 0.4 } })], 'test') applyOps(doc, [routeEdit({ view: 'phone', slide: 'slideaaaaaaa', block: view(doc).slides[0].blocks[0], field: 'hidden', value: true })], 'test') const after = view(doc).slides[0].blocks[0] assert.deepEqual(after.phone, { hidden: true, box: { x: 0, y: 0, w: 1, h: 0.4 } }) assert.deepEqual(after.box, block.box) assert.equal(hasOverride(after), true) applyOps(doc, [resetPhone('slideaaaaaaa', after)], 'test') assert.deepEqual(view(doc).slides[0].blocks[0].phone, {}) assert.equal(hasOverride(view(doc).slides[0].blocks[0]), false) }) test('phoneMap reads only the four keys, and only well-typed', () => { assert.deepEqual(phoneMap({ phone: { hidden: 'yes', order: 1.5, size: '', box: null, other: 1 } }), {}) assert.deepEqual(phoneMap({ phone: { hidden: false, order: -1, size: 'l' } }), { hidden: false, order: -1, size: 'l' }) assert.deepEqual(phoneMap({}), {}) }) test('the stored view is read defensively', () => { const mem = new Map() const storage = { getItem: k => mem.get(k) ?? null, setItem: (k, v) => mem.set(k, v) } assert.equal(readView(storage), 'desktop') writeView(storage, 'phone') assert.equal(mem.get(STORAGE_KEY), 'phone') assert.equal(readView(storage), 'phone') mem.set(STORAGE_KEY, 'tablet') assert.equal(readView(storage), 'desktop') const broken = { getItem () { throw new Error('private') }, setItem () { throw new Error('quota') } } assert.equal(readView(broken), 'desktop') assert.doesNotThrow(() => writeView(broken, 'both')) })