| 1 | // switcher.mjs's rule: in the phone view an edit to a block's box, |
| 2 | // size, order or hidden becomes set_phone over the whole override map; |
| 3 | // on the desktop, box and size are the block's own. Plus the stored |
| 4 | // view, read defensively. |
| 5 | import test from 'node:test' |
| 6 | import assert from 'node:assert/strict' |
| 7 | import * as Y from 'yjs' |
| 8 | import { applyOps, view } from './ops.mjs' |
| 9 | import { routeEdit, phoneMap, resetPhone, hasOverride, readView, writeView, STORAGE_KEY } from './switcher.mjs' |
| 10 | |
| 11 | 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: {} } |
| 12 | const withPhone = { ...block, phone: { hidden: true, order: 2, size: 's', box: { x: 0, y: 0, w: 1, h: 0.5 } } } |
| 13 | |
| 14 | test('desktop view: box and size are the block; hidden and order are always the phone map', () => { |
| 15 | assert.deepEqual(routeEdit({ view: 'desktop', slide: 's', block, field: 'box', value: { x: 0.2, y: 0.2, w: 0.5, h: 0.3 } }), |
| 16 | { op: 'set_block', slide: 's', id: block.id, box: { x: 0.2, y: 0.2, w: 0.5, h: 0.3 } }) |
| 17 | assert.deepEqual(routeEdit({ view: 'desktop', slide: 's', block, field: 'size', value: 'l' }), |
| 18 | { op: 'set_block', slide: 's', id: block.id, props: { size: 'l' } }) |
| 19 | assert.deepEqual(routeEdit({ view: 'desktop', slide: 's', block, field: 'size', value: '' }), |
| 20 | { op: 'set_block', slide: 's', id: block.id, props: { size: null } }) |
| 21 | assert.deepEqual(routeEdit({ view: 'desktop', slide: 's', block, field: 'hidden', value: true }), |
| 22 | { op: 'set_phone', slide: 's', id: block.id, phone: { hidden: true } }) |
| 23 | assert.deepEqual(routeEdit({ view: 'both', slide: 's', block, field: 'order', value: 3 }), |
| 24 | { op: 'set_phone', slide: 's', id: block.id, phone: { order: 3 } }) |
| 25 | }) |
| 26 | |
| 27 | test('phone view: everything goes to set_phone, carrying the rest of the map', () => { |
| 28 | assert.deepEqual(routeEdit({ view: 'phone', slide: 's', block: withPhone, field: 'box', value: { x: 0.12345, y: 0, w: 0.5, h: 0.5 } }), |
| 29 | { 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 } } }) |
| 30 | assert.deepEqual(routeEdit({ view: 'phone', slide: 's', block: withPhone, field: 'size', value: 'l' }).phone.size, 'l') |
| 31 | assert.deepEqual(routeEdit({ view: 'phone', slide: 's', block: withPhone, field: 'hidden', value: false }).phone, |
| 32 | { order: 2, size: 's', box: { x: 0, y: 0, w: 1, h: 0.5 } }) |
| 33 | assert.deepEqual(routeEdit({ view: 'phone', slide: 's', block: withPhone, field: 'order', value: null }).phone, |
| 34 | { hidden: true, size: 's', box: { x: 0, y: 0, w: 1, h: 0.5 } }) |
| 35 | assert.deepEqual(routeEdit({ view: 'phone', slide: 's', block, field: 'size', value: 'l' }), |
| 36 | { op: 'set_phone', slide: 's', id: block.id, phone: { size: 'l' } }) |
| 37 | assert.throws(() => routeEdit({ view: 'phone', slide: 's', block, field: 'level', value: 2 })) |
| 38 | }) |
| 39 | |
| 40 | test('the ops it makes are ones the document takes', () => { |
| 41 | const doc = new Y.Doc() |
| 42 | applyOps(doc, [ |
| 43 | { op: 'add_slide', id: 'slideaaaaaaa', layout: 'canvas' }, |
| 44 | { op: 'add_block', slide: 'slideaaaaaaa', id: block.id, type: 'text', box: block.box, z: 0, markdown: 'x' } |
| 45 | ], 'test') |
| 46 | const b = view(doc).slides[0].blocks[0] |
| 47 | applyOps(doc, [routeEdit({ view: 'phone', slide: 'slideaaaaaaa', block: b, field: 'box', value: { x: 0, y: 0, w: 1, h: 0.4 } })], 'test') |
| 48 | applyOps(doc, [routeEdit({ view: 'phone', slide: 'slideaaaaaaa', block: view(doc).slides[0].blocks[0], field: 'hidden', value: true })], 'test') |
| 49 | const after = view(doc).slides[0].blocks[0] |
| 50 | assert.deepEqual(after.phone, { hidden: true, box: { x: 0, y: 0, w: 1, h: 0.4 } }) |
| 51 | assert.deepEqual(after.box, block.box) |
| 52 | assert.equal(hasOverride(after), true) |
| 53 | applyOps(doc, [resetPhone('slideaaaaaaa', after)], 'test') |
| 54 | assert.deepEqual(view(doc).slides[0].blocks[0].phone, {}) |
| 55 | assert.equal(hasOverride(view(doc).slides[0].blocks[0]), false) |
| 56 | }) |
| 57 | |
| 58 | test('phoneMap reads only the four keys, and only well-typed', () => { |
| 59 | assert.deepEqual(phoneMap({ phone: { hidden: 'yes', order: 1.5, size: '', box: null, other: 1 } }), {}) |
| 60 | assert.deepEqual(phoneMap({ phone: { hidden: false, order: -1, size: 'l' } }), { hidden: false, order: -1, size: 'l' }) |
| 61 | assert.deepEqual(phoneMap({}), {}) |
| 62 | }) |
| 63 | |
| 64 | test('the stored view is read defensively', () => { |
| 65 | const mem = new Map() |
| 66 | const storage = { getItem: k => mem.get(k) ?? null, setItem: (k, v) => mem.set(k, v) } |
| 67 | assert.equal(readView(storage), 'desktop') |
| 68 | writeView(storage, 'phone') |
| 69 | assert.equal(mem.get(STORAGE_KEY), 'phone') |
| 70 | assert.equal(readView(storage), 'phone') |
| 71 | mem.set(STORAGE_KEY, 'tablet') |
| 72 | assert.equal(readView(storage), 'desktop') |
| 73 | const broken = { getItem () { throw new Error('private') }, setItem () { throw new Error('quota') } } |
| 74 | assert.equal(readView(broken), 'desktop') |
| 75 | assert.doesNotThrow(() => writeView(broken, 'both')) |
| 76 | }) |
| 77 | |