Independent instance state persistence (#1493)
This PR
- Removes UserDocumentRecordType
- moving isSnapMode to user preferences
- moving isGridMode and isPenMode to InstanceRecordType
- deleting the other properties which are no longer needed.
- Creates a separate pipeline for persisting instance state.
Previously the instance state records were stored alongside the document
state records, and in order to load the state for a particular instance
(in our case, a particular tab) you needed to pass the 'instanceId'
prop. This prop ended up totally pervading the public API and people ran
into all kinds of issues with it, e.g. using the same instance id in
multiple editor instances.
There was also an issue whereby it was hard for us to clean up old
instance state so the idb table ended up bloating over time.
This PR makes it so that rather than passing an instanceId, you load the
instance state yourself while creating the store. It provides tools to
make that easy.
- Undoes the assumption that we might have more than one instance's
state in the store.
- Like `document`, `instance` now has a singleton id
`instance:instance`.
- Page state ids and camera ids are no longer random, but rather derive
from the page they belong to. This is like having a foreign primary key
in SQL databases. It's something i'd love to support fully as part of
the RecordType/Store api.
Tests to do
- [x] Test Migrations
- [x] Test Store.listen filtering
- [x] Make type sets in Store public and readonly
- [x] Test RecordType.createId
- [x] Test Instance state snapshot loading/exporting
- [x] Manual test File I/O
- [x] Manual test Vscode extension with multiple tabs
- [x] Audit usages of store.query
- [x] Audit usages of changed types: InstanceRecordType, 'instance',
InstancePageStateRecordType, 'instance_page_state', 'user_document',
'camera', CameraRecordType, InstancePresenceRecordType,
'instance_presence'
- [x] Test user preferences
- [x] Manual test isSnapMode and isGridMode and isPenMode
- [ ] Test indexedDb functions
- [x] Add instanceId stuff back
### Change Type
- [x] `major` — Breaking Change
### Test Plan
1. Add a step-by-step description of how to test your PR here.
2.
- [ ] Unit Tests
- [ ] Webdriver tests
### Release Notes
- Add a brief release note for your PR here.
2023-06-05 14:11:07 +00:00
|
|
|
import { Migrations, Store, createRecordType } from '@tldraw/store'
|
2023-04-25 11:01:25 +00:00
|
|
|
import fs from 'fs'
|
|
|
|
import { imageAssetMigrations } from './assets/TLImageAsset'
|
|
|
|
import { videoAssetMigrations } from './assets/TLVideoAsset'
|
2023-06-03 20:46:53 +00:00
|
|
|
import { documentMigrations } from './records/TLDocument'
|
|
|
|
import { instanceMigrations, instanceTypeVersions } from './records/TLInstance'
|
Independent instance state persistence (#1493)
This PR
- Removes UserDocumentRecordType
- moving isSnapMode to user preferences
- moving isGridMode and isPenMode to InstanceRecordType
- deleting the other properties which are no longer needed.
- Creates a separate pipeline for persisting instance state.
Previously the instance state records were stored alongside the document
state records, and in order to load the state for a particular instance
(in our case, a particular tab) you needed to pass the 'instanceId'
prop. This prop ended up totally pervading the public API and people ran
into all kinds of issues with it, e.g. using the same instance id in
multiple editor instances.
There was also an issue whereby it was hard for us to clean up old
instance state so the idb table ended up bloating over time.
This PR makes it so that rather than passing an instanceId, you load the
instance state yourself while creating the store. It provides tools to
make that easy.
- Undoes the assumption that we might have more than one instance's
state in the store.
- Like `document`, `instance` now has a singleton id
`instance:instance`.
- Page state ids and camera ids are no longer random, but rather derive
from the page they belong to. This is like having a foreign primary key
in SQL databases. It's something i'd love to support fully as part of
the RecordType/Store api.
Tests to do
- [x] Test Migrations
- [x] Test Store.listen filtering
- [x] Make type sets in Store public and readonly
- [x] Test RecordType.createId
- [x] Test Instance state snapshot loading/exporting
- [x] Manual test File I/O
- [x] Manual test Vscode extension with multiple tabs
- [x] Audit usages of store.query
- [x] Audit usages of changed types: InstanceRecordType, 'instance',
InstancePageStateRecordType, 'instance_page_state', 'user_document',
'camera', CameraRecordType, InstancePresenceRecordType,
'instance_presence'
- [x] Test user preferences
- [x] Manual test isSnapMode and isGridMode and isPenMode
- [ ] Test indexedDb functions
- [x] Add instanceId stuff back
### Change Type
- [x] `major` — Breaking Change
### Test Plan
1. Add a step-by-step description of how to test your PR here.
2.
- [ ] Unit Tests
- [ ] Webdriver tests
### Release Notes
- Add a brief release note for your PR here.
2023-06-05 14:11:07 +00:00
|
|
|
import { instancePageStateMigrations, instancePageStateVersions } from './records/TLPageState'
|
|
|
|
import { instancePresenceMigrations, instancePresenceVersions } from './records/TLPresence'
|
2023-06-06 16:15:12 +00:00
|
|
|
import { TLShape, rootShapeMigrations, Versions as rootShapeVersions } from './records/TLShape'
|
2023-06-03 20:46:53 +00:00
|
|
|
import { arrowShapeMigrations } from './shapes/TLArrowShape'
|
|
|
|
import { bookmarkShapeMigrations } from './shapes/TLBookmarkShape'
|
|
|
|
import { drawShapeMigrations } from './shapes/TLDrawShape'
|
|
|
|
import { embedShapeMigrations } from './shapes/TLEmbedShape'
|
|
|
|
import { geoShapeMigrations } from './shapes/TLGeoShape'
|
|
|
|
import { imageShapeMigrations } from './shapes/TLImageShape'
|
|
|
|
import { noteShapeMigrations } from './shapes/TLNoteShape'
|
|
|
|
import { textShapeMigrations } from './shapes/TLTextShape'
|
|
|
|
import { videoShapeMigrations } from './shapes/TLVideoShape'
|
|
|
|
import { storeMigrations, storeVersions } from './store-migrations'
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
const assetModules = fs
|
|
|
|
.readdirSync('src/assets')
|
|
|
|
.filter((n) => n.match(/^TL.*\.ts$/))
|
|
|
|
.map((f) => [f, require(`./assets/${f.slice(0, -3)}`)])
|
|
|
|
const shapeModules = fs
|
|
|
|
.readdirSync('src/shapes')
|
|
|
|
.filter((n) => n.match(/^TL.*\.ts$/))
|
|
|
|
.map((f) => [f, require(`./shapes/${f.slice(0, -3)}`)])
|
|
|
|
const recordModules = fs
|
|
|
|
.readdirSync('src/records')
|
|
|
|
.filter((n) => n.match(/^TL.*\.ts$/))
|
|
|
|
.map((f) => [f, require(`./records/${f.slice(0, -3)}`)])
|
|
|
|
|
|
|
|
const allModules = [
|
|
|
|
...assetModules,
|
|
|
|
...shapeModules,
|
|
|
|
...recordModules,
|
2023-06-03 20:46:53 +00:00
|
|
|
['store-migrations.ts', require('./store-migrations')],
|
2023-04-25 11:01:25 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
const allMigrators: Array<{
|
|
|
|
fileName: string
|
|
|
|
version: number
|
|
|
|
up: jest.SpyInstance
|
|
|
|
down: jest.SpyInstance
|
|
|
|
}> = []
|
|
|
|
|
|
|
|
for (const [fileName, module] of allModules) {
|
|
|
|
const migrationsKey = Object.keys(module).find((k) => k.endsWith('igrations'))
|
|
|
|
|
|
|
|
if (!migrationsKey) continue
|
|
|
|
|
|
|
|
const migrations: Migrations = module[migrationsKey]
|
|
|
|
|
|
|
|
for (const version of Object.keys(migrations.migrators)) {
|
|
|
|
const originalUp = migrations.migrators[version as any].up
|
|
|
|
const originalDown = migrations.migrators[version as any].down
|
|
|
|
const up = jest
|
|
|
|
.spyOn(migrations.migrators[version as any], 'up')
|
|
|
|
.mockImplementation((initialRecord) => {
|
|
|
|
if (initialRecord instanceof Store) return originalUp(initialRecord)
|
|
|
|
|
|
|
|
const clonedRecord = structuredClone(initialRecord)
|
|
|
|
const result = originalUp(initialRecord)
|
|
|
|
// mutations should never mutate their input
|
|
|
|
expect(initialRecord).toEqual(clonedRecord)
|
|
|
|
return result
|
|
|
|
})
|
|
|
|
const down = jest
|
|
|
|
.spyOn(migrations.migrators[version as any], 'down')
|
|
|
|
.mockImplementation((initialRecord) => {
|
|
|
|
if (initialRecord instanceof Store) return originalDown(initialRecord)
|
|
|
|
|
|
|
|
const clonedRecord = structuredClone(initialRecord)
|
|
|
|
const result = originalDown(initialRecord)
|
|
|
|
// mutations should never mutate their input
|
|
|
|
expect(initialRecord).toEqual(clonedRecord)
|
|
|
|
return result
|
|
|
|
})
|
|
|
|
allMigrators.push({
|
|
|
|
fileName,
|
|
|
|
version: Number(version),
|
|
|
|
up,
|
|
|
|
down,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
test('all modules export migrations', () => {
|
|
|
|
const modulesWithoutMigrations = allModules
|
2023-05-22 21:46:24 +00:00
|
|
|
.filter(([, module]) => {
|
|
|
|
return !Object.keys(module).find((k) => k.endsWith('igrations'))
|
|
|
|
})
|
2023-04-25 11:01:25 +00:00
|
|
|
.map(([fileName]) => fileName)
|
2023-06-03 20:46:53 +00:00
|
|
|
.filter((n) => !(n === 'TLBaseAsset.ts' || n === 'TLBaseShape.ts' || n === 'TLRecord.ts'))
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
// IF THIS LINE IS FAILING YOU NEED TO MAKE SURE THE MIGRATIONS ARE EXPORTED
|
|
|
|
expect(modulesWithoutMigrations).toHaveLength(0)
|
|
|
|
})
|
|
|
|
|
|
|
|
/* --- PUT YOUR MIGRATIONS TESTS BELOW HERE --- */
|
|
|
|
|
|
|
|
describe('TLVideoAsset AddIsAnimated', () => {
|
|
|
|
const oldAsset = {
|
|
|
|
id: '1',
|
|
|
|
type: 'video',
|
|
|
|
props: {
|
|
|
|
src: 'https://www.youtube.com/watch?v=1',
|
|
|
|
name: 'video',
|
|
|
|
width: 100,
|
|
|
|
height: 100,
|
|
|
|
mimeType: 'video/mp4',
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
const newAsset = {
|
|
|
|
id: '1',
|
|
|
|
type: 'video',
|
|
|
|
props: {
|
|
|
|
src: 'https://www.youtube.com/watch?v=1',
|
|
|
|
name: 'video',
|
|
|
|
width: 100,
|
|
|
|
height: 100,
|
|
|
|
mimeType: 'video/mp4',
|
|
|
|
isAnimated: false,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
const { up, down } = videoAssetMigrations.migrators[1]
|
|
|
|
|
|
|
|
test('up works as expected', () => {
|
|
|
|
expect(up(oldAsset)).toEqual(newAsset)
|
|
|
|
})
|
|
|
|
test('down works as expected', () => {
|
|
|
|
expect(down(newAsset)).toEqual(oldAsset)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
describe('TLImageAsset AddIsAnimated', () => {
|
|
|
|
const oldAsset = {
|
|
|
|
id: '1',
|
|
|
|
type: 'image',
|
|
|
|
props: {
|
|
|
|
src: 'https://www.youtube.com/watch?v=1',
|
|
|
|
name: 'image',
|
|
|
|
width: 100,
|
|
|
|
height: 100,
|
|
|
|
mimeType: 'image/gif',
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
const newAsset = {
|
|
|
|
id: '1',
|
|
|
|
type: 'image',
|
|
|
|
props: {
|
|
|
|
src: 'https://www.youtube.com/watch?v=1',
|
|
|
|
name: 'image',
|
|
|
|
width: 100,
|
|
|
|
height: 100,
|
|
|
|
mimeType: 'image/gif',
|
|
|
|
isAnimated: false,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
const { up, down } = imageAssetMigrations.migrators[1]
|
|
|
|
|
|
|
|
test('up works as expected', () => {
|
|
|
|
expect(up(oldAsset)).toEqual(newAsset)
|
|
|
|
})
|
|
|
|
test('down works as expected', () => {
|
|
|
|
expect(down(newAsset)).toEqual(oldAsset)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
const ShapeRecord = createRecordType('shape', {
|
|
|
|
validator: { validate: (record) => record as TLShape },
|
derived presence state (#1204)
This PR adds
- A new `TLInstancePresence` record type, to collect info about the
presence state in a particular instance of the editor. This will
eventually be used to sync presence data instead of sending
instance-only state across the wire.
- **Record Scopes**
`RecordType` now has a `scope` property which can be one of three
things:
- `document`: the record belongs to the document and should be synced
and persisted freely. Currently: `TLDocument`, `TLPage`, `TLShape`, and
`TLAsset`
- `instance`: the record belongs to a single instance of the store and
should not be synced at all. It should not be persisted directly in most
cases, but rather compiled into a kind of 'instance configuration' to
store alongside the local document data so that when reopening the
associated document it can remember some of the previous instance state.
Currently: `TLInstance`, `TLInstancePageState`, `TLCamera`, `TLUser`,
`TLUserDocument`, `TLUserPresence`
- `presence`: the record belongs to a single instance of the store and
should not be persisted, but may be synced using the special presence
sync protocol. Currently just `TLInstancePresence`
This sets us up for the following changes, which are gonna be pretty
high-impact in terms of integrating tldraw into existing systems:
- Removing `instanceId` as a config option. Each instance gets a
randomly generated ID.
- We'd replace it with an `instanceConfig` option that has stuff like
selectedIds, camera positions, and so on. Then it's up to library users
to get and reinstate the instance config at persistence boundaries.
- Removing `userId` as config option, and removing the `TLUser` type
altogether.
- We might need to revisit when doing auth-enabled features like locking
shapes, but I suspect that will be separate.
2023-04-27 18:03:19 +00:00
|
|
|
scope: 'document',
|
2023-04-25 11:01:25 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
describe('Store removing Icon and Code shapes', () => {
|
|
|
|
test('up works as expected', () => {
|
|
|
|
const snapshot = Object.fromEntries(
|
|
|
|
[
|
|
|
|
ShapeRecord.create({
|
|
|
|
type: 'icon',
|
|
|
|
parentId: 'page:any',
|
|
|
|
index: 'a0',
|
|
|
|
props: { name: 'a' },
|
|
|
|
} as any),
|
|
|
|
ShapeRecord.create({
|
|
|
|
type: 'icon',
|
|
|
|
parentId: 'page:any',
|
|
|
|
index: 'a0',
|
|
|
|
props: { name: 'b' },
|
|
|
|
} as any),
|
|
|
|
ShapeRecord.create({
|
|
|
|
type: 'code',
|
|
|
|
parentId: 'page:any',
|
|
|
|
index: 'a0',
|
|
|
|
props: { name: 'c' },
|
|
|
|
} as any),
|
|
|
|
ShapeRecord.create({
|
|
|
|
type: 'code',
|
|
|
|
parentId: 'page:any',
|
|
|
|
index: 'a0',
|
|
|
|
props: { name: 'd' },
|
|
|
|
} as any),
|
|
|
|
ShapeRecord.create({
|
|
|
|
type: 'geo',
|
|
|
|
parentId: 'page:any',
|
|
|
|
index: 'a0',
|
|
|
|
props: { geo: 'rectangle', w: 1, h: 1, growY: 1, text: '' },
|
|
|
|
} as any),
|
|
|
|
].map((shape) => [shape.id, shape])
|
|
|
|
)
|
2023-05-25 09:54:29 +00:00
|
|
|
const fixed = storeMigrations.migrators[storeVersions.RemoveCodeAndIconShapeTypes].up(snapshot)
|
2023-04-25 11:01:25 +00:00
|
|
|
expect(Object.entries(fixed)).toHaveLength(1)
|
|
|
|
})
|
|
|
|
|
|
|
|
test('down works as expected', () => {
|
|
|
|
const snapshot = Object.fromEntries(
|
|
|
|
[
|
|
|
|
ShapeRecord.create({
|
|
|
|
type: 'geo',
|
|
|
|
parentId: 'page:any',
|
|
|
|
index: 'a0',
|
|
|
|
props: { geo: 'rectangle', name: 'e', w: 1, h: 1, growY: 1, text: '' },
|
|
|
|
} as any),
|
|
|
|
].map((shape) => [shape.id, shape])
|
|
|
|
)
|
|
|
|
|
2023-05-25 09:54:29 +00:00
|
|
|
storeMigrations.migrators[storeVersions.RemoveCodeAndIconShapeTypes].down(snapshot)
|
2023-04-25 11:01:25 +00:00
|
|
|
expect(Object.entries(snapshot)).toHaveLength(1)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
describe('Adding export background', () => {
|
2023-06-03 20:46:53 +00:00
|
|
|
const { up, down } = instanceMigrations.migrators[1]
|
2023-04-25 11:01:25 +00:00
|
|
|
test('up works as expected', () => {
|
|
|
|
const before = {}
|
|
|
|
const after = { exportBackground: true }
|
|
|
|
expect(up(before)).toStrictEqual(after)
|
|
|
|
})
|
|
|
|
|
|
|
|
test('down works as expected', () => {
|
|
|
|
const before = { exportBackground: true }
|
|
|
|
const after = {}
|
|
|
|
expect(down(before)).toStrictEqual(after)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
describe('Removing dialogs from instance', () => {
|
2023-06-03 20:46:53 +00:00
|
|
|
const { up, down } = instanceMigrations.migrators[2]
|
2023-04-25 11:01:25 +00:00
|
|
|
test('up works as expected', () => {
|
|
|
|
const before = { dialog: null }
|
|
|
|
const after = {}
|
|
|
|
expect(up(before)).toStrictEqual(after)
|
|
|
|
})
|
|
|
|
|
|
|
|
test('down works as expected', () => {
|
|
|
|
const before = {}
|
|
|
|
const after = { dialog: null }
|
|
|
|
expect(down(before)).toStrictEqual(after)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
describe('Adding url props', () => {
|
|
|
|
for (const [name, { up, down }] of [
|
2023-06-03 20:46:53 +00:00
|
|
|
['video shape', videoShapeMigrations.migrators[1]],
|
|
|
|
['note shape', noteShapeMigrations.migrators[1]],
|
|
|
|
['geo shape', geoShapeMigrations.migrators[1]],
|
|
|
|
['image shape', imageShapeMigrations.migrators[1]],
|
2023-04-25 11:01:25 +00:00
|
|
|
] as const) {
|
|
|
|
test(`${name}: up works as expected`, () => {
|
|
|
|
const before = { props: {} }
|
|
|
|
const after = { props: { url: '' } }
|
|
|
|
expect(up(before)).toStrictEqual(after)
|
|
|
|
})
|
|
|
|
|
|
|
|
test(`${name}: down works as expected`, () => {
|
|
|
|
const before = { props: { url: '' } }
|
|
|
|
const after = { props: {} }
|
|
|
|
expect(down(before)).toStrictEqual(after)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
describe('Bookmark null asset id', () => {
|
2023-06-03 20:46:53 +00:00
|
|
|
const { up, down } = bookmarkShapeMigrations.migrators[1]
|
2023-04-25 11:01:25 +00:00
|
|
|
test('up works as expected', () => {
|
|
|
|
const before = { props: {} }
|
|
|
|
const after = { props: { assetId: null } }
|
|
|
|
expect(up(before)).toStrictEqual(after)
|
|
|
|
})
|
|
|
|
|
|
|
|
test('down works as expected', () => {
|
|
|
|
const before = { props: { assetId: null } }
|
|
|
|
const after = { props: {} }
|
|
|
|
expect(down(before)).toStrictEqual(after)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
describe('Renaming asset props', () => {
|
|
|
|
for (const [name, { up, down }] of [
|
|
|
|
['image shape', imageAssetMigrations.migrators[2]],
|
|
|
|
['video shape', videoAssetMigrations.migrators[2]],
|
|
|
|
] as const) {
|
|
|
|
test(`${name}: up works as expected`, () => {
|
|
|
|
const before = { props: { width: 100, height: 100 } }
|
|
|
|
const after = { props: { w: 100, h: 100 } }
|
|
|
|
expect(up(before)).toStrictEqual(after)
|
|
|
|
})
|
|
|
|
|
|
|
|
test(`${name}: down works as expected`, () => {
|
|
|
|
const before = { props: { w: 100, h: 100 } }
|
|
|
|
const after = { props: { width: 100, height: 100 } }
|
|
|
|
expect(down(before)).toStrictEqual(after)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
describe('Adding instance.isToolLocked', () => {
|
2023-06-03 20:46:53 +00:00
|
|
|
const { up, down } = instanceMigrations.migrators[3]
|
2023-04-25 11:01:25 +00:00
|
|
|
test('up works as expected', () => {
|
|
|
|
expect(up({})).toMatchObject({ isToolLocked: false })
|
|
|
|
expect(up({ isToolLocked: true })).toMatchObject({ isToolLocked: false })
|
|
|
|
})
|
|
|
|
|
|
|
|
test('down works as expected', () => {
|
|
|
|
expect(down({ isToolLocked: true })).toStrictEqual({})
|
|
|
|
expect(down({ isToolLocked: false })).toStrictEqual({})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
describe('Cleaning up junk data in instance.propsForNextShape', () => {
|
2023-06-03 20:46:53 +00:00
|
|
|
const { up, down } = instanceMigrations.migrators[4]
|
2023-04-25 11:01:25 +00:00
|
|
|
test('up works as expected', () => {
|
|
|
|
expect(up({ propsForNextShape: { color: 'red', unknown: 'gone' } })).toEqual({
|
|
|
|
propsForNextShape: {
|
|
|
|
color: 'red',
|
|
|
|
},
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
test('down works as expected', () => {
|
|
|
|
const instance = { propsForNextShape: { color: 'red' } }
|
|
|
|
expect(down(instance)).toBe(instance)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
describe('Generating original URL from embed URL in GenOriginalUrlInEmbed', () => {
|
2023-06-03 20:46:53 +00:00
|
|
|
const { up, down } = embedShapeMigrations.migrators[1]
|
2023-04-25 11:01:25 +00:00
|
|
|
test('up works as expected', () => {
|
|
|
|
expect(up({ props: { url: 'https://codepen.io/Rplus/embed/PWZYRM' } })).toEqual({
|
|
|
|
props: {
|
|
|
|
url: 'https://codepen.io/Rplus/pen/PWZYRM',
|
|
|
|
tmpOldUrl: 'https://codepen.io/Rplus/embed/PWZYRM',
|
|
|
|
},
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
test('invalid up works as expected', () => {
|
|
|
|
expect(up({ props: { url: 'https://example.com' } })).toEqual({
|
|
|
|
props: {
|
|
|
|
url: '',
|
|
|
|
tmpOldUrl: 'https://example.com',
|
|
|
|
},
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
test('down works as expected', () => {
|
|
|
|
const instance = {
|
|
|
|
props: {
|
|
|
|
url: 'https://codepen.io/Rplus/pen/PWZYRM',
|
|
|
|
tmpOldUrl: 'https://codepen.io/Rplus/embed/PWZYRM',
|
|
|
|
},
|
|
|
|
}
|
|
|
|
expect(down(instance)).toEqual({ props: { url: 'https://codepen.io/Rplus/embed/PWZYRM' } })
|
|
|
|
})
|
|
|
|
|
|
|
|
test('invalid down works as expected', () => {
|
|
|
|
const instance = {
|
|
|
|
props: {
|
|
|
|
url: 'https://example.com',
|
|
|
|
tmpOldUrl: '',
|
|
|
|
},
|
|
|
|
}
|
|
|
|
expect(down(instance)).toEqual({ props: { url: '' } })
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
describe('Adding isPen prop', () => {
|
2023-06-03 20:46:53 +00:00
|
|
|
const { up, down } = drawShapeMigrations.migrators[1]
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
test('up works as expected with a shape that is not a pen shape', () => {
|
|
|
|
expect(
|
|
|
|
up({
|
|
|
|
props: {
|
|
|
|
segments: [
|
|
|
|
{
|
|
|
|
type: 'free',
|
|
|
|
points: [
|
|
|
|
{ x: 0, y: 0, z: 0.5 },
|
|
|
|
{ x: 1, y: 1, z: 0.5 },
|
|
|
|
],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
})
|
|
|
|
).toEqual({
|
|
|
|
props: {
|
|
|
|
isPen: false,
|
|
|
|
segments: [
|
|
|
|
{
|
|
|
|
type: 'free',
|
|
|
|
points: [
|
|
|
|
{ x: 0, y: 0, z: 0.5 },
|
|
|
|
{ x: 1, y: 1, z: 0.5 },
|
|
|
|
],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
test('up works as expected when converting to pen', () => {
|
|
|
|
expect(
|
|
|
|
up({
|
|
|
|
props: {
|
|
|
|
segments: [
|
|
|
|
{
|
|
|
|
type: 'free',
|
|
|
|
points: [
|
|
|
|
{ x: 0, y: 0, z: 0.2315 },
|
|
|
|
{ x: 1, y: 1, z: 0.2421 },
|
|
|
|
],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
})
|
|
|
|
).toEqual({
|
|
|
|
props: {
|
|
|
|
isPen: true,
|
|
|
|
segments: [
|
|
|
|
{
|
|
|
|
type: 'free',
|
|
|
|
points: [
|
|
|
|
{ x: 0, y: 0, z: 0.2315 },
|
|
|
|
{ x: 1, y: 1, z: 0.2421 },
|
|
|
|
],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
test('down works as expected', () => {
|
|
|
|
expect(down({ props: { isPen: false } })).toEqual({
|
|
|
|
props: {},
|
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
describe('Adding isLocked prop', () => {
|
2023-06-03 20:46:53 +00:00
|
|
|
const { up, down } = rootShapeMigrations.migrators[1]
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
test('up works as expected', () => {
|
|
|
|
expect(up({})).toEqual({ isLocked: false })
|
|
|
|
})
|
|
|
|
|
|
|
|
test('down works as expected', () => {
|
|
|
|
expect(down({ isLocked: false })).toEqual({})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
describe('Adding labelColor prop to geo / arrow shapes', () => {
|
|
|
|
for (const [name, { up, down }] of [
|
2023-06-03 20:46:53 +00:00
|
|
|
['arrow shape', arrowShapeMigrations.migrators[1]],
|
|
|
|
['geo shape', geoShapeMigrations.migrators[2]],
|
2023-04-25 11:01:25 +00:00
|
|
|
] as const) {
|
|
|
|
test(`${name}: up works as expected`, () => {
|
|
|
|
expect(up({ props: { color: 'red' } })).toEqual({
|
|
|
|
props: { color: 'red', labelColor: 'black' },
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
test(`${name}: down works as expected`, () => {
|
|
|
|
expect(down({ props: { color: 'red', labelColor: 'blue' } })).toEqual({
|
|
|
|
props: { color: 'red' },
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
describe('Adding labelColor prop to propsForNextShape', () => {
|
2023-06-03 20:46:53 +00:00
|
|
|
const { up, down } = instanceMigrations.migrators[5]
|
2023-04-25 11:01:25 +00:00
|
|
|
test('up works as expected', () => {
|
|
|
|
expect(up({ propsForNextShape: { color: 'red' } })).toEqual({
|
|
|
|
propsForNextShape: { color: 'red', labelColor: 'black' },
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
test('down works as expected', () => {
|
|
|
|
expect(down({ propsForNextShape: { color: 'red', labelColor: 'blue' } })).toEqual({
|
|
|
|
propsForNextShape: { color: 'red' },
|
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
describe('Adding croppingId to instancePageState', () => {
|
|
|
|
const { up, down } = instancePageStateMigrations.migrators[1]
|
|
|
|
test('up works as expected', () => {
|
|
|
|
expect(up({})).toEqual({
|
|
|
|
croppingId: null,
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
test('down works as expected', () => {
|
|
|
|
expect(down({ croppingId: null })).toEqual({})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
describe('Adding followingUserId prop to instance', () => {
|
2023-06-03 20:46:53 +00:00
|
|
|
const { up, down } = instanceMigrations.migrators[6]
|
2023-04-25 11:01:25 +00:00
|
|
|
test('up works as expected', () => {
|
|
|
|
expect(up({})).toEqual({ followingUserId: null })
|
|
|
|
})
|
|
|
|
|
|
|
|
test('down works as expected', () => {
|
|
|
|
expect(down({ followingUserId: '123' })).toEqual({})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
describe('Removing align=justify from propsForNextShape', () => {
|
2023-06-03 20:46:53 +00:00
|
|
|
const { up, down } = instanceMigrations.migrators[7]
|
2023-04-25 11:01:25 +00:00
|
|
|
test('up works as expected', () => {
|
|
|
|
expect(up({ propsForNextShape: { color: 'black', align: 'justify' } })).toEqual({
|
|
|
|
propsForNextShape: { color: 'black', align: 'start' },
|
|
|
|
})
|
|
|
|
expect(up({ propsForNextShape: { color: 'black', align: 'end' } })).toEqual({
|
|
|
|
propsForNextShape: { color: 'black', align: 'end' },
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
test('down works as expected', () => {
|
|
|
|
expect(down({ propsForNextShape: { color: 'black', align: 'end' } })).toEqual({
|
|
|
|
propsForNextShape: { color: 'black', align: 'end' },
|
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
describe('Adding zoomBrush prop to instance', () => {
|
2023-06-03 20:46:53 +00:00
|
|
|
const { up, down } = instanceMigrations.migrators[8]
|
2023-04-25 11:01:25 +00:00
|
|
|
test('up works as expected', () => {
|
|
|
|
expect(up({})).toEqual({ zoomBrush: null })
|
|
|
|
})
|
|
|
|
|
|
|
|
test('down works as expected', () => {
|
|
|
|
expect(down({ zoomBrush: { x: 1, y: 2, w: 3, h: 4 } })).toEqual({})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
describe('Removing align=justify from shape align props', () => {
|
|
|
|
for (const [name, { up, down }] of [
|
2023-06-03 20:46:53 +00:00
|
|
|
['text', textShapeMigrations.migrators[1]],
|
|
|
|
['note', noteShapeMigrations.migrators[2]],
|
|
|
|
['geo', geoShapeMigrations.migrators[3]],
|
2023-04-25 11:01:25 +00:00
|
|
|
] as const) {
|
|
|
|
test(`${name}: up works as expected`, () => {
|
|
|
|
expect(up({ props: { align: 'justify' } })).toEqual({
|
|
|
|
props: { align: 'start' },
|
|
|
|
})
|
|
|
|
expect(up({ props: { align: 'end' } })).toEqual({
|
|
|
|
props: { align: 'end' },
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
test(`${name}: down works as expected`, () => {
|
|
|
|
expect(down({ props: { align: 'start' } })).toEqual({
|
|
|
|
props: { align: 'start' },
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
describe('Add crop=null to image shapes', () => {
|
2023-06-03 20:46:53 +00:00
|
|
|
const { up, down } = imageShapeMigrations.migrators[2]
|
2023-04-25 11:01:25 +00:00
|
|
|
test('up works as expected', () => {
|
|
|
|
expect(up({ props: { w: 100 } })).toEqual({
|
|
|
|
props: { w: 100, crop: null },
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
test('down works as expected', () => {
|
|
|
|
expect(down({ props: { w: 100, crop: null } })).toEqual({
|
|
|
|
props: { w: 100 },
|
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
derived presence state (#1204)
This PR adds
- A new `TLInstancePresence` record type, to collect info about the
presence state in a particular instance of the editor. This will
eventually be used to sync presence data instead of sending
instance-only state across the wire.
- **Record Scopes**
`RecordType` now has a `scope` property which can be one of three
things:
- `document`: the record belongs to the document and should be synced
and persisted freely. Currently: `TLDocument`, `TLPage`, `TLShape`, and
`TLAsset`
- `instance`: the record belongs to a single instance of the store and
should not be synced at all. It should not be persisted directly in most
cases, but rather compiled into a kind of 'instance configuration' to
store alongside the local document data so that when reopening the
associated document it can remember some of the previous instance state.
Currently: `TLInstance`, `TLInstancePageState`, `TLCamera`, `TLUser`,
`TLUserDocument`, `TLUserPresence`
- `presence`: the record belongs to a single instance of the store and
should not be persisted, but may be synced using the special presence
sync protocol. Currently just `TLInstancePresence`
This sets us up for the following changes, which are gonna be pretty
high-impact in terms of integrating tldraw into existing systems:
- Removing `instanceId` as a config option. Each instance gets a
randomly generated ID.
- We'd replace it with an `instanceConfig` option that has stuff like
selectedIds, camera positions, and so on. Then it's up to library users
to get and reinstate the instance config at persistence boundaries.
- Removing `userId` as config option, and removing the `TLUser` type
altogether.
- We might need to revisit when doing auth-enabled features like locking
shapes, but I suspect that will be separate.
2023-04-27 18:03:19 +00:00
|
|
|
describe('Adding instance_presence to the schema', () => {
|
2023-05-25 09:54:29 +00:00
|
|
|
const { up, down } = storeMigrations.migrators[storeVersions.AddInstancePresenceType]
|
derived presence state (#1204)
This PR adds
- A new `TLInstancePresence` record type, to collect info about the
presence state in a particular instance of the editor. This will
eventually be used to sync presence data instead of sending
instance-only state across the wire.
- **Record Scopes**
`RecordType` now has a `scope` property which can be one of three
things:
- `document`: the record belongs to the document and should be synced
and persisted freely. Currently: `TLDocument`, `TLPage`, `TLShape`, and
`TLAsset`
- `instance`: the record belongs to a single instance of the store and
should not be synced at all. It should not be persisted directly in most
cases, but rather compiled into a kind of 'instance configuration' to
store alongside the local document data so that when reopening the
associated document it can remember some of the previous instance state.
Currently: `TLInstance`, `TLInstancePageState`, `TLCamera`, `TLUser`,
`TLUserDocument`, `TLUserPresence`
- `presence`: the record belongs to a single instance of the store and
should not be persisted, but may be synced using the special presence
sync protocol. Currently just `TLInstancePresence`
This sets us up for the following changes, which are gonna be pretty
high-impact in terms of integrating tldraw into existing systems:
- Removing `instanceId` as a config option. Each instance gets a
randomly generated ID.
- We'd replace it with an `instanceConfig` option that has stuff like
selectedIds, camera positions, and so on. Then it's up to library users
to get and reinstate the instance config at persistence boundaries.
- Removing `userId` as config option, and removing the `TLUser` type
altogether.
- We might need to revisit when doing auth-enabled features like locking
shapes, but I suspect that will be separate.
2023-04-27 18:03:19 +00:00
|
|
|
|
|
|
|
test('up works as expected', () => {
|
|
|
|
expect(up({})).toEqual({})
|
|
|
|
})
|
|
|
|
test('down works as expected', () => {
|
|
|
|
expect(
|
|
|
|
down({
|
|
|
|
'instance_presence:123': { id: 'instance_presence:123', typeName: 'instance_presence' },
|
|
|
|
'instance:123': { id: 'instance:123', typeName: 'instance' },
|
|
|
|
})
|
|
|
|
).toEqual({
|
|
|
|
'instance:123': { id: 'instance:123', typeName: 'instance' },
|
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2023-06-01 18:46:26 +00:00
|
|
|
describe('Adding name to document', () => {
|
2023-06-03 20:46:53 +00:00
|
|
|
const { up, down } = documentMigrations.migrators[1]
|
2023-06-01 18:46:26 +00:00
|
|
|
|
|
|
|
test('up works as expected', () => {
|
|
|
|
expect(up({})).toEqual({ name: '' })
|
|
|
|
})
|
|
|
|
|
|
|
|
test('down works as expected', () => {
|
|
|
|
expect(down({ name: '' })).toEqual({})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2023-05-09 13:32:04 +00:00
|
|
|
describe('Adding check-box to geo shape', () => {
|
2023-06-03 20:46:53 +00:00
|
|
|
const { up, down } = geoShapeMigrations.migrators[4]
|
2023-05-09 13:32:04 +00:00
|
|
|
|
|
|
|
test('up works as expected', () => {
|
|
|
|
expect(up({ props: { geo: 'rectangle' } })).toEqual({ props: { geo: 'rectangle' } })
|
|
|
|
})
|
|
|
|
test('down works as expected', () => {
|
|
|
|
expect(down({ props: { geo: 'rectangle' } })).toEqual({ props: { geo: 'rectangle' } })
|
|
|
|
expect(down({ props: { geo: 'check-box' } })).toEqual({ props: { geo: 'rectangle' } })
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2023-05-19 10:23:43 +00:00
|
|
|
describe('Add verticalAlign to geo shape', () => {
|
2023-06-03 20:46:53 +00:00
|
|
|
const { up, down } = geoShapeMigrations.migrators[5]
|
2023-05-19 10:23:43 +00:00
|
|
|
|
|
|
|
test('up works as expected', () => {
|
|
|
|
expect(up({ props: { type: 'ellipse' } })).toEqual({
|
|
|
|
props: { type: 'ellipse', verticalAlign: 'middle' },
|
|
|
|
})
|
|
|
|
})
|
|
|
|
test('down works as expected', () => {
|
|
|
|
expect(down({ props: { verticalAlign: 'middle', type: 'ellipse' } })).toEqual({
|
|
|
|
props: { type: 'ellipse' },
|
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
describe('Add verticalAlign to props for next shape', () => {
|
2023-06-03 20:46:53 +00:00
|
|
|
const { up, down } = instanceMigrations.migrators[9]
|
2023-05-19 10:23:43 +00:00
|
|
|
test('up works as expected', () => {
|
|
|
|
expect(up({ propsForNextShape: { color: 'red' } })).toEqual({
|
|
|
|
propsForNextShape: {
|
|
|
|
color: 'red',
|
|
|
|
verticalAlign: 'middle',
|
|
|
|
},
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
test('down works as expected', () => {
|
|
|
|
const instance = { propsForNextShape: { color: 'red', verticalAlign: 'middle' } }
|
|
|
|
expect(down(instance)).toEqual({
|
|
|
|
propsForNextShape: {
|
|
|
|
color: 'red',
|
|
|
|
},
|
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2023-05-24 11:34:13 +00:00
|
|
|
describe('Migrate GeoShape legacy horizontal alignment', () => {
|
2023-06-03 20:46:53 +00:00
|
|
|
const { up, down } = geoShapeMigrations.migrators[6]
|
2023-05-24 11:34:13 +00:00
|
|
|
|
|
|
|
test('up works as expected', () => {
|
|
|
|
expect(up({ props: { align: 'start', type: 'ellipse' } })).toEqual({
|
|
|
|
props: { align: 'start-legacy', type: 'ellipse' },
|
|
|
|
})
|
|
|
|
expect(up({ props: { align: 'middle', type: 'ellipse' } })).toEqual({
|
|
|
|
props: { align: 'middle-legacy', type: 'ellipse' },
|
|
|
|
})
|
|
|
|
expect(up({ props: { align: 'end', type: 'ellipse' } })).toEqual({
|
|
|
|
props: { align: 'end-legacy', type: 'ellipse' },
|
|
|
|
})
|
|
|
|
})
|
|
|
|
test('down works as expected', () => {
|
|
|
|
expect(down({ props: { align: 'start-legacy', type: 'ellipse' } })).toEqual({
|
|
|
|
props: { align: 'start', type: 'ellipse' },
|
|
|
|
})
|
|
|
|
expect(down({ props: { align: 'middle-legacy', type: 'ellipse' } })).toEqual({
|
|
|
|
props: { align: 'middle', type: 'ellipse' },
|
|
|
|
})
|
|
|
|
expect(down({ props: { align: 'end-legacy', type: 'ellipse' } })).toEqual({
|
|
|
|
props: { align: 'end', type: 'ellipse' },
|
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
describe('Migrate NoteShape legacy horizontal alignment', () => {
|
2023-06-03 20:46:53 +00:00
|
|
|
const { up, down } = noteShapeMigrations.migrators[3]
|
2023-05-24 11:34:13 +00:00
|
|
|
|
|
|
|
test('up works as expected', () => {
|
|
|
|
expect(up({ props: { align: 'start', color: 'red' } })).toEqual({
|
|
|
|
props: { align: 'start-legacy', color: 'red' },
|
|
|
|
})
|
|
|
|
expect(up({ props: { align: 'middle', color: 'red' } })).toEqual({
|
|
|
|
props: { align: 'middle-legacy', color: 'red' },
|
|
|
|
})
|
|
|
|
expect(up({ props: { align: 'end', color: 'red' } })).toEqual({
|
|
|
|
props: { align: 'end-legacy', color: 'red' },
|
|
|
|
})
|
|
|
|
})
|
|
|
|
test('down works as expected', () => {
|
|
|
|
expect(down({ props: { align: 'start-legacy', color: 'red' } })).toEqual({
|
|
|
|
props: { align: 'start', color: 'red' },
|
|
|
|
})
|
|
|
|
expect(down({ props: { align: 'middle-legacy', color: 'red' } })).toEqual({
|
|
|
|
props: { align: 'middle', color: 'red' },
|
|
|
|
})
|
|
|
|
expect(down({ props: { align: 'end-legacy', color: 'red' } })).toEqual({
|
|
|
|
props: { align: 'end', color: 'red' },
|
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2023-05-19 11:09:13 +00:00
|
|
|
describe('Adds delay to scribble', () => {
|
2023-06-03 20:46:53 +00:00
|
|
|
const { up, down } = instanceMigrations.migrators[10]
|
2023-05-19 11:09:13 +00:00
|
|
|
|
|
|
|
test('up has no effect when scribble is null', () => {
|
|
|
|
expect(
|
|
|
|
up({
|
|
|
|
scribble: null,
|
|
|
|
})
|
|
|
|
).toEqual({ scribble: null })
|
|
|
|
})
|
|
|
|
|
|
|
|
test('up adds the delay property', () => {
|
|
|
|
expect(
|
|
|
|
up({
|
|
|
|
scribble: {
|
|
|
|
points: [{ x: 0, y: 0 }],
|
|
|
|
size: 4,
|
|
|
|
color: 'black',
|
|
|
|
opacity: 1,
|
|
|
|
state: 'starting',
|
|
|
|
},
|
|
|
|
})
|
|
|
|
).toEqual({
|
|
|
|
scribble: {
|
|
|
|
points: [{ x: 0, y: 0 }],
|
|
|
|
size: 4,
|
|
|
|
color: 'black',
|
|
|
|
opacity: 1,
|
|
|
|
state: 'starting',
|
|
|
|
delay: 0,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
test('down has no effect when scribble is null', () => {
|
|
|
|
expect(down({ scribble: null })).toEqual({ scribble: null })
|
|
|
|
})
|
|
|
|
|
|
|
|
test('removes the delay property', () => {
|
|
|
|
expect(
|
|
|
|
down({
|
|
|
|
scribble: {
|
|
|
|
points: [{ x: 0, y: 0 }],
|
|
|
|
size: 4,
|
|
|
|
color: 'black',
|
|
|
|
opacity: 1,
|
|
|
|
state: 'starting',
|
|
|
|
delay: 0,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
).toEqual({
|
|
|
|
scribble: {
|
|
|
|
points: [{ x: 0, y: 0 }],
|
|
|
|
size: 4,
|
|
|
|
color: 'black',
|
|
|
|
opacity: 1,
|
|
|
|
state: 'starting',
|
|
|
|
},
|
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
describe('Adds delay to scribble', () => {
|
2023-06-03 20:46:53 +00:00
|
|
|
const { up, down } = instancePresenceMigrations.migrators[1]
|
2023-05-19 11:09:13 +00:00
|
|
|
|
|
|
|
test('up has no effect when scribble is null', () => {
|
|
|
|
expect(
|
|
|
|
up({
|
|
|
|
scribble: null,
|
|
|
|
})
|
|
|
|
).toEqual({ scribble: null })
|
|
|
|
})
|
|
|
|
|
|
|
|
test('up adds the delay property', () => {
|
|
|
|
expect(
|
|
|
|
up({
|
|
|
|
scribble: {
|
|
|
|
points: [{ x: 0, y: 0 }],
|
|
|
|
size: 4,
|
|
|
|
color: 'black',
|
|
|
|
opacity: 1,
|
|
|
|
state: 'starting',
|
|
|
|
},
|
|
|
|
})
|
|
|
|
).toEqual({
|
|
|
|
scribble: {
|
|
|
|
points: [{ x: 0, y: 0 }],
|
|
|
|
size: 4,
|
|
|
|
color: 'black',
|
|
|
|
opacity: 1,
|
|
|
|
state: 'starting',
|
|
|
|
delay: 0,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
test('down has no effect when scribble is null', () => {
|
|
|
|
expect(down({ scribble: null })).toEqual({ scribble: null })
|
|
|
|
})
|
|
|
|
|
|
|
|
test('removes the delay property', () => {
|
|
|
|
expect(
|
|
|
|
down({
|
|
|
|
scribble: {
|
|
|
|
points: [{ x: 0, y: 0 }],
|
|
|
|
size: 4,
|
|
|
|
color: 'black',
|
|
|
|
opacity: 1,
|
|
|
|
state: 'starting',
|
|
|
|
delay: 0,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
).toEqual({
|
|
|
|
scribble: {
|
|
|
|
points: [{ x: 0, y: 0 }],
|
|
|
|
size: 4,
|
|
|
|
color: 'black',
|
|
|
|
opacity: 1,
|
|
|
|
state: 'starting',
|
|
|
|
},
|
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
2023-05-17 10:45:43 +00:00
|
|
|
|
2023-05-25 09:54:29 +00:00
|
|
|
describe('user config refactor', () => {
|
|
|
|
test('removes user and user_presence types from snapshots', () => {
|
|
|
|
const { up, down } =
|
|
|
|
storeMigrations.migrators[storeVersions.RemoveTLUserAndPresenceAndAddPointer]
|
|
|
|
|
|
|
|
const prevSnapshot = {
|
|
|
|
'user:123': {
|
|
|
|
id: 'user:123',
|
|
|
|
typeName: 'user',
|
|
|
|
},
|
|
|
|
'user_presence:123': {
|
|
|
|
id: 'user_presence:123',
|
|
|
|
typeName: 'user_presence',
|
|
|
|
},
|
|
|
|
'instance:123': {
|
|
|
|
id: 'instance:123',
|
|
|
|
typeName: 'instance',
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
const nextSnapshot = {
|
|
|
|
'instance:123': {
|
|
|
|
id: 'instance:123',
|
|
|
|
typeName: 'instance',
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
// up removes the user and user_presence types
|
|
|
|
expect(up(prevSnapshot)).toEqual(nextSnapshot)
|
|
|
|
// down cannot add them back so it should be a no-op
|
|
|
|
expect(
|
|
|
|
down({
|
|
|
|
...nextSnapshot,
|
|
|
|
'pointer:134': {
|
|
|
|
id: 'pointer:134',
|
|
|
|
typeName: 'pointer',
|
|
|
|
},
|
|
|
|
})
|
|
|
|
).toEqual(nextSnapshot)
|
|
|
|
})
|
|
|
|
|
|
|
|
test('removes userId from the instance state', () => {
|
2023-06-03 20:46:53 +00:00
|
|
|
const { up, down } = instanceMigrations.migrators[instanceTypeVersions.RemoveUserId]
|
2023-05-25 09:54:29 +00:00
|
|
|
|
|
|
|
const prev = {
|
|
|
|
id: 'instance:123',
|
|
|
|
typeName: 'instance',
|
|
|
|
userId: 'user:123',
|
|
|
|
}
|
|
|
|
|
|
|
|
const next = {
|
|
|
|
id: 'instance:123',
|
|
|
|
typeName: 'instance',
|
|
|
|
}
|
|
|
|
|
|
|
|
expect(up(prev)).toEqual(next)
|
|
|
|
// it cannot be added back so it should add some meaningless id in there
|
|
|
|
// in practice, because we bumped the store version, this down migrator will never be used
|
|
|
|
expect(down(next)).toMatchInlineSnapshot(`
|
|
|
|
Object {
|
|
|
|
"id": "instance:123",
|
|
|
|
"typeName": "instance",
|
|
|
|
"userId": "user:none",
|
|
|
|
}
|
|
|
|
`)
|
|
|
|
})
|
Independent instance state persistence (#1493)
This PR
- Removes UserDocumentRecordType
- moving isSnapMode to user preferences
- moving isGridMode and isPenMode to InstanceRecordType
- deleting the other properties which are no longer needed.
- Creates a separate pipeline for persisting instance state.
Previously the instance state records were stored alongside the document
state records, and in order to load the state for a particular instance
(in our case, a particular tab) you needed to pass the 'instanceId'
prop. This prop ended up totally pervading the public API and people ran
into all kinds of issues with it, e.g. using the same instance id in
multiple editor instances.
There was also an issue whereby it was hard for us to clean up old
instance state so the idb table ended up bloating over time.
This PR makes it so that rather than passing an instanceId, you load the
instance state yourself while creating the store. It provides tools to
make that easy.
- Undoes the assumption that we might have more than one instance's
state in the store.
- Like `document`, `instance` now has a singleton id
`instance:instance`.
- Page state ids and camera ids are no longer random, but rather derive
from the page they belong to. This is like having a foreign primary key
in SQL databases. It's something i'd love to support fully as part of
the RecordType/Store api.
Tests to do
- [x] Test Migrations
- [x] Test Store.listen filtering
- [x] Make type sets in Store public and readonly
- [x] Test RecordType.createId
- [x] Test Instance state snapshot loading/exporting
- [x] Manual test File I/O
- [x] Manual test Vscode extension with multiple tabs
- [x] Audit usages of store.query
- [x] Audit usages of changed types: InstanceRecordType, 'instance',
InstancePageStateRecordType, 'instance_page_state', 'user_document',
'camera', CameraRecordType, InstancePresenceRecordType,
'instance_presence'
- [x] Test user preferences
- [x] Manual test isSnapMode and isGridMode and isPenMode
- [ ] Test indexedDb functions
- [x] Add instanceId stuff back
### Change Type
- [x] `major` — Breaking Change
### Test Plan
1. Add a step-by-step description of how to test your PR here.
2.
- [ ] Unit Tests
- [ ] Webdriver tests
### Release Notes
- Add a brief release note for your PR here.
2023-06-05 14:11:07 +00:00
|
|
|
})
|
2023-05-25 09:54:29 +00:00
|
|
|
|
Independent instance state persistence (#1493)
This PR
- Removes UserDocumentRecordType
- moving isSnapMode to user preferences
- moving isGridMode and isPenMode to InstanceRecordType
- deleting the other properties which are no longer needed.
- Creates a separate pipeline for persisting instance state.
Previously the instance state records were stored alongside the document
state records, and in order to load the state for a particular instance
(in our case, a particular tab) you needed to pass the 'instanceId'
prop. This prop ended up totally pervading the public API and people ran
into all kinds of issues with it, e.g. using the same instance id in
multiple editor instances.
There was also an issue whereby it was hard for us to clean up old
instance state so the idb table ended up bloating over time.
This PR makes it so that rather than passing an instanceId, you load the
instance state yourself while creating the store. It provides tools to
make that easy.
- Undoes the assumption that we might have more than one instance's
state in the store.
- Like `document`, `instance` now has a singleton id
`instance:instance`.
- Page state ids and camera ids are no longer random, but rather derive
from the page they belong to. This is like having a foreign primary key
in SQL databases. It's something i'd love to support fully as part of
the RecordType/Store api.
Tests to do
- [x] Test Migrations
- [x] Test Store.listen filtering
- [x] Make type sets in Store public and readonly
- [x] Test RecordType.createId
- [x] Test Instance state snapshot loading/exporting
- [x] Manual test File I/O
- [x] Manual test Vscode extension with multiple tabs
- [x] Audit usages of store.query
- [x] Audit usages of changed types: InstanceRecordType, 'instance',
InstancePageStateRecordType, 'instance_page_state', 'user_document',
'camera', CameraRecordType, InstancePresenceRecordType,
'instance_presence'
- [x] Test user preferences
- [x] Manual test isSnapMode and isGridMode and isPenMode
- [ ] Test indexedDb functions
- [x] Add instanceId stuff back
### Change Type
- [x] `major` — Breaking Change
### Test Plan
1. Add a step-by-step description of how to test your PR here.
2.
- [ ] Unit Tests
- [ ] Webdriver tests
### Release Notes
- Add a brief release note for your PR here.
2023-06-05 14:11:07 +00:00
|
|
|
describe('making instance state independent', () => {
|
|
|
|
it('adds isPenMode and isGridMode to instance state', () => {
|
2023-05-25 09:54:29 +00:00
|
|
|
const { up, down } =
|
Independent instance state persistence (#1493)
This PR
- Removes UserDocumentRecordType
- moving isSnapMode to user preferences
- moving isGridMode and isPenMode to InstanceRecordType
- deleting the other properties which are no longer needed.
- Creates a separate pipeline for persisting instance state.
Previously the instance state records were stored alongside the document
state records, and in order to load the state for a particular instance
(in our case, a particular tab) you needed to pass the 'instanceId'
prop. This prop ended up totally pervading the public API and people ran
into all kinds of issues with it, e.g. using the same instance id in
multiple editor instances.
There was also an issue whereby it was hard for us to clean up old
instance state so the idb table ended up bloating over time.
This PR makes it so that rather than passing an instanceId, you load the
instance state yourself while creating the store. It provides tools to
make that easy.
- Undoes the assumption that we might have more than one instance's
state in the store.
- Like `document`, `instance` now has a singleton id
`instance:instance`.
- Page state ids and camera ids are no longer random, but rather derive
from the page they belong to. This is like having a foreign primary key
in SQL databases. It's something i'd love to support fully as part of
the RecordType/Store api.
Tests to do
- [x] Test Migrations
- [x] Test Store.listen filtering
- [x] Make type sets in Store public and readonly
- [x] Test RecordType.createId
- [x] Test Instance state snapshot loading/exporting
- [x] Manual test File I/O
- [x] Manual test Vscode extension with multiple tabs
- [x] Audit usages of store.query
- [x] Audit usages of changed types: InstanceRecordType, 'instance',
InstancePageStateRecordType, 'instance_page_state', 'user_document',
'camera', CameraRecordType, InstancePresenceRecordType,
'instance_presence'
- [x] Test user preferences
- [x] Manual test isSnapMode and isGridMode and isPenMode
- [ ] Test indexedDb functions
- [x] Add instanceId stuff back
### Change Type
- [x] `major` — Breaking Change
### Test Plan
1. Add a step-by-step description of how to test your PR here.
2.
- [ ] Unit Tests
- [ ] Webdriver tests
### Release Notes
- Add a brief release note for your PR here.
2023-06-05 14:11:07 +00:00
|
|
|
instanceMigrations.migrators[instanceTypeVersions.AddIsPenModeAndIsGridMode]
|
2023-05-25 09:54:29 +00:00
|
|
|
|
|
|
|
const prev = {
|
Independent instance state persistence (#1493)
This PR
- Removes UserDocumentRecordType
- moving isSnapMode to user preferences
- moving isGridMode and isPenMode to InstanceRecordType
- deleting the other properties which are no longer needed.
- Creates a separate pipeline for persisting instance state.
Previously the instance state records were stored alongside the document
state records, and in order to load the state for a particular instance
(in our case, a particular tab) you needed to pass the 'instanceId'
prop. This prop ended up totally pervading the public API and people ran
into all kinds of issues with it, e.g. using the same instance id in
multiple editor instances.
There was also an issue whereby it was hard for us to clean up old
instance state so the idb table ended up bloating over time.
This PR makes it so that rather than passing an instanceId, you load the
instance state yourself while creating the store. It provides tools to
make that easy.
- Undoes the assumption that we might have more than one instance's
state in the store.
- Like `document`, `instance` now has a singleton id
`instance:instance`.
- Page state ids and camera ids are no longer random, but rather derive
from the page they belong to. This is like having a foreign primary key
in SQL databases. It's something i'd love to support fully as part of
the RecordType/Store api.
Tests to do
- [x] Test Migrations
- [x] Test Store.listen filtering
- [x] Make type sets in Store public and readonly
- [x] Test RecordType.createId
- [x] Test Instance state snapshot loading/exporting
- [x] Manual test File I/O
- [x] Manual test Vscode extension with multiple tabs
- [x] Audit usages of store.query
- [x] Audit usages of changed types: InstanceRecordType, 'instance',
InstancePageStateRecordType, 'instance_page_state', 'user_document',
'camera', CameraRecordType, InstancePresenceRecordType,
'instance_presence'
- [x] Test user preferences
- [x] Manual test isSnapMode and isGridMode and isPenMode
- [ ] Test indexedDb functions
- [x] Add instanceId stuff back
### Change Type
- [x] `major` — Breaking Change
### Test Plan
1. Add a step-by-step description of how to test your PR here.
2.
- [ ] Unit Tests
- [ ] Webdriver tests
### Release Notes
- Add a brief release note for your PR here.
2023-06-05 14:11:07 +00:00
|
|
|
id: 'instance:123',
|
|
|
|
typeName: 'instance',
|
2023-05-25 09:54:29 +00:00
|
|
|
}
|
|
|
|
const next = {
|
Independent instance state persistence (#1493)
This PR
- Removes UserDocumentRecordType
- moving isSnapMode to user preferences
- moving isGridMode and isPenMode to InstanceRecordType
- deleting the other properties which are no longer needed.
- Creates a separate pipeline for persisting instance state.
Previously the instance state records were stored alongside the document
state records, and in order to load the state for a particular instance
(in our case, a particular tab) you needed to pass the 'instanceId'
prop. This prop ended up totally pervading the public API and people ran
into all kinds of issues with it, e.g. using the same instance id in
multiple editor instances.
There was also an issue whereby it was hard for us to clean up old
instance state so the idb table ended up bloating over time.
This PR makes it so that rather than passing an instanceId, you load the
instance state yourself while creating the store. It provides tools to
make that easy.
- Undoes the assumption that we might have more than one instance's
state in the store.
- Like `document`, `instance` now has a singleton id
`instance:instance`.
- Page state ids and camera ids are no longer random, but rather derive
from the page they belong to. This is like having a foreign primary key
in SQL databases. It's something i'd love to support fully as part of
the RecordType/Store api.
Tests to do
- [x] Test Migrations
- [x] Test Store.listen filtering
- [x] Make type sets in Store public and readonly
- [x] Test RecordType.createId
- [x] Test Instance state snapshot loading/exporting
- [x] Manual test File I/O
- [x] Manual test Vscode extension with multiple tabs
- [x] Audit usages of store.query
- [x] Audit usages of changed types: InstanceRecordType, 'instance',
InstancePageStateRecordType, 'instance_page_state', 'user_document',
'camera', CameraRecordType, InstancePresenceRecordType,
'instance_presence'
- [x] Test user preferences
- [x] Manual test isSnapMode and isGridMode and isPenMode
- [ ] Test indexedDb functions
- [x] Add instanceId stuff back
### Change Type
- [x] `major` — Breaking Change
### Test Plan
1. Add a step-by-step description of how to test your PR here.
2.
- [ ] Unit Tests
- [ ] Webdriver tests
### Release Notes
- Add a brief release note for your PR here.
2023-06-05 14:11:07 +00:00
|
|
|
id: 'instance:123',
|
|
|
|
typeName: 'instance',
|
|
|
|
isPenMode: false,
|
2023-05-25 09:54:29 +00:00
|
|
|
isGridMode: false,
|
|
|
|
}
|
|
|
|
|
|
|
|
expect(up(prev)).toEqual(next)
|
Independent instance state persistence (#1493)
This PR
- Removes UserDocumentRecordType
- moving isSnapMode to user preferences
- moving isGridMode and isPenMode to InstanceRecordType
- deleting the other properties which are no longer needed.
- Creates a separate pipeline for persisting instance state.
Previously the instance state records were stored alongside the document
state records, and in order to load the state for a particular instance
(in our case, a particular tab) you needed to pass the 'instanceId'
prop. This prop ended up totally pervading the public API and people ran
into all kinds of issues with it, e.g. using the same instance id in
multiple editor instances.
There was also an issue whereby it was hard for us to clean up old
instance state so the idb table ended up bloating over time.
This PR makes it so that rather than passing an instanceId, you load the
instance state yourself while creating the store. It provides tools to
make that easy.
- Undoes the assumption that we might have more than one instance's
state in the store.
- Like `document`, `instance` now has a singleton id
`instance:instance`.
- Page state ids and camera ids are no longer random, but rather derive
from the page they belong to. This is like having a foreign primary key
in SQL databases. It's something i'd love to support fully as part of
the RecordType/Store api.
Tests to do
- [x] Test Migrations
- [x] Test Store.listen filtering
- [x] Make type sets in Store public and readonly
- [x] Test RecordType.createId
- [x] Test Instance state snapshot loading/exporting
- [x] Manual test File I/O
- [x] Manual test Vscode extension with multiple tabs
- [x] Audit usages of store.query
- [x] Audit usages of changed types: InstanceRecordType, 'instance',
InstancePageStateRecordType, 'instance_page_state', 'user_document',
'camera', CameraRecordType, InstancePresenceRecordType,
'instance_presence'
- [x] Test user preferences
- [x] Manual test isSnapMode and isGridMode and isPenMode
- [ ] Test indexedDb functions
- [x] Add instanceId stuff back
### Change Type
- [x] `major` — Breaking Change
### Test Plan
1. Add a step-by-step description of how to test your PR here.
2.
- [ ] Unit Tests
- [ ] Webdriver tests
### Release Notes
- Add a brief release note for your PR here.
2023-06-05 14:11:07 +00:00
|
|
|
expect(down(next)).toEqual(prev)
|
|
|
|
})
|
|
|
|
|
|
|
|
it('removes instanceId and cameraId from instancePageState', () => {
|
|
|
|
const { up, down } =
|
|
|
|
instancePageStateMigrations.migrators[instancePageStateVersions.RemoveInstanceIdAndCameraId]
|
|
|
|
|
|
|
|
const prev = {
|
|
|
|
id: 'instance_page_state:123',
|
|
|
|
typeName: 'instance_page_state',
|
|
|
|
instanceId: 'instance:123',
|
|
|
|
cameraId: 'camera:123',
|
|
|
|
selectedIds: [],
|
|
|
|
}
|
|
|
|
|
|
|
|
const next = {
|
|
|
|
id: 'instance_page_state:123',
|
|
|
|
typeName: 'instance_page_state',
|
|
|
|
selectedIds: [],
|
|
|
|
}
|
|
|
|
|
|
|
|
expect(up(prev)).toEqual(next)
|
|
|
|
// down should never be called
|
2023-05-25 09:54:29 +00:00
|
|
|
expect(down(next)).toMatchInlineSnapshot(`
|
|
|
|
Object {
|
Independent instance state persistence (#1493)
This PR
- Removes UserDocumentRecordType
- moving isSnapMode to user preferences
- moving isGridMode and isPenMode to InstanceRecordType
- deleting the other properties which are no longer needed.
- Creates a separate pipeline for persisting instance state.
Previously the instance state records were stored alongside the document
state records, and in order to load the state for a particular instance
(in our case, a particular tab) you needed to pass the 'instanceId'
prop. This prop ended up totally pervading the public API and people ran
into all kinds of issues with it, e.g. using the same instance id in
multiple editor instances.
There was also an issue whereby it was hard for us to clean up old
instance state so the idb table ended up bloating over time.
This PR makes it so that rather than passing an instanceId, you load the
instance state yourself while creating the store. It provides tools to
make that easy.
- Undoes the assumption that we might have more than one instance's
state in the store.
- Like `document`, `instance` now has a singleton id
`instance:instance`.
- Page state ids and camera ids are no longer random, but rather derive
from the page they belong to. This is like having a foreign primary key
in SQL databases. It's something i'd love to support fully as part of
the RecordType/Store api.
Tests to do
- [x] Test Migrations
- [x] Test Store.listen filtering
- [x] Make type sets in Store public and readonly
- [x] Test RecordType.createId
- [x] Test Instance state snapshot loading/exporting
- [x] Manual test File I/O
- [x] Manual test Vscode extension with multiple tabs
- [x] Audit usages of store.query
- [x] Audit usages of changed types: InstanceRecordType, 'instance',
InstancePageStateRecordType, 'instance_page_state', 'user_document',
'camera', CameraRecordType, InstancePresenceRecordType,
'instance_presence'
- [x] Test user preferences
- [x] Manual test isSnapMode and isGridMode and isPenMode
- [ ] Test indexedDb functions
- [x] Add instanceId stuff back
### Change Type
- [x] `major` — Breaking Change
### Test Plan
1. Add a step-by-step description of how to test your PR here.
2.
- [ ] Unit Tests
- [ ] Webdriver tests
### Release Notes
- Add a brief release note for your PR here.
2023-06-05 14:11:07 +00:00
|
|
|
"cameraId": "camera:void",
|
|
|
|
"id": "instance_page_state:123",
|
|
|
|
"instanceId": "instance:instance",
|
|
|
|
"selectedIds": Array [],
|
|
|
|
"typeName": "instance_page_state",
|
2023-05-25 09:54:29 +00:00
|
|
|
}
|
|
|
|
`)
|
|
|
|
})
|
Independent instance state persistence (#1493)
This PR
- Removes UserDocumentRecordType
- moving isSnapMode to user preferences
- moving isGridMode and isPenMode to InstanceRecordType
- deleting the other properties which are no longer needed.
- Creates a separate pipeline for persisting instance state.
Previously the instance state records were stored alongside the document
state records, and in order to load the state for a particular instance
(in our case, a particular tab) you needed to pass the 'instanceId'
prop. This prop ended up totally pervading the public API and people ran
into all kinds of issues with it, e.g. using the same instance id in
multiple editor instances.
There was also an issue whereby it was hard for us to clean up old
instance state so the idb table ended up bloating over time.
This PR makes it so that rather than passing an instanceId, you load the
instance state yourself while creating the store. It provides tools to
make that easy.
- Undoes the assumption that we might have more than one instance's
state in the store.
- Like `document`, `instance` now has a singleton id
`instance:instance`.
- Page state ids and camera ids are no longer random, but rather derive
from the page they belong to. This is like having a foreign primary key
in SQL databases. It's something i'd love to support fully as part of
the RecordType/Store api.
Tests to do
- [x] Test Migrations
- [x] Test Store.listen filtering
- [x] Make type sets in Store public and readonly
- [x] Test RecordType.createId
- [x] Test Instance state snapshot loading/exporting
- [x] Manual test File I/O
- [x] Manual test Vscode extension with multiple tabs
- [x] Audit usages of store.query
- [x] Audit usages of changed types: InstanceRecordType, 'instance',
InstancePageStateRecordType, 'instance_page_state', 'user_document',
'camera', CameraRecordType, InstancePresenceRecordType,
'instance_presence'
- [x] Test user preferences
- [x] Manual test isSnapMode and isGridMode and isPenMode
- [ ] Test indexedDb functions
- [x] Add instanceId stuff back
### Change Type
- [x] `major` — Breaking Change
### Test Plan
1. Add a step-by-step description of how to test your PR here.
2.
- [ ] Unit Tests
- [ ] Webdriver tests
### Release Notes
- Add a brief release note for your PR here.
2023-06-05 14:11:07 +00:00
|
|
|
|
|
|
|
it('removes instanceId from instancePresence', () => {
|
|
|
|
const { up, down } =
|
|
|
|
instancePresenceMigrations.migrators[instancePresenceVersions.RemoveInstanceId]
|
|
|
|
|
|
|
|
const prev = {
|
|
|
|
id: 'instance_presence:123',
|
|
|
|
typeName: 'instance_presence',
|
|
|
|
instanceId: 'instance:123',
|
|
|
|
selectedIds: [],
|
|
|
|
}
|
|
|
|
|
|
|
|
const next = {
|
|
|
|
id: 'instance_presence:123',
|
|
|
|
typeName: 'instance_presence',
|
|
|
|
selectedIds: [],
|
|
|
|
}
|
|
|
|
|
|
|
|
expect(up(prev)).toEqual(next)
|
|
|
|
|
|
|
|
// down should never be called
|
|
|
|
expect(down(next)).toMatchInlineSnapshot(`
|
|
|
|
Object {
|
|
|
|
"id": "instance_presence:123",
|
|
|
|
"instanceId": "instance:instance",
|
|
|
|
"selectedIds": Array [],
|
|
|
|
"typeName": "instance_presence",
|
|
|
|
}
|
|
|
|
`)
|
|
|
|
})
|
|
|
|
|
|
|
|
it('removes userDocument from the schema', () => {
|
|
|
|
const { up, down } = storeMigrations.migrators[storeVersions.RemoveUserDocument]
|
|
|
|
|
|
|
|
const prev = {
|
|
|
|
'user_document:123': {
|
|
|
|
id: 'user_document:123',
|
|
|
|
typeName: 'user_document',
|
|
|
|
},
|
|
|
|
'instance:123': {
|
|
|
|
id: 'instance:123',
|
|
|
|
typeName: 'instance',
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
const next = {
|
|
|
|
'instance:123': {
|
|
|
|
id: 'instance:123',
|
|
|
|
typeName: 'instance',
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
expect(up(prev)).toEqual(next)
|
|
|
|
expect(down(next)).toEqual(next)
|
|
|
|
})
|
2023-05-25 09:54:29 +00:00
|
|
|
})
|
|
|
|
|
2023-06-06 13:27:32 +00:00
|
|
|
describe('Adds NoteShape vertical alignment', () => {
|
|
|
|
const { up, down } = noteShapeMigrations.migrators[4]
|
|
|
|
|
|
|
|
test('up works as expected', () => {
|
|
|
|
expect(up({ props: { color: 'red' } })).toEqual({
|
|
|
|
props: { verticalAlign: 'middle', color: 'red' },
|
|
|
|
})
|
|
|
|
})
|
|
|
|
test('down works as expected', () => {
|
|
|
|
expect(down({ props: { verticalAlign: 'top', color: 'red' } })).toEqual({
|
|
|
|
props: { color: 'red' },
|
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2023-06-06 16:15:12 +00:00
|
|
|
describe('hoist opacity', () => {
|
|
|
|
test('hoists opacity from a shape to another', () => {
|
|
|
|
const { up, down } = rootShapeMigrations.migrators[rootShapeVersions.HoistOpacity]
|
|
|
|
const before = {
|
|
|
|
type: 'myShape',
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
props: {
|
|
|
|
color: 'red',
|
|
|
|
opacity: '0.5',
|
|
|
|
},
|
|
|
|
}
|
|
|
|
const after = {
|
|
|
|
type: 'myShape',
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
opacity: 0.5,
|
|
|
|
props: {
|
|
|
|
color: 'red',
|
|
|
|
},
|
|
|
|
}
|
|
|
|
const afterWithNonMatchingOpacity = {
|
|
|
|
type: 'myShape',
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
opacity: 0.6,
|
|
|
|
props: {
|
|
|
|
color: 'red',
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
expect(up(before)).toEqual(after)
|
|
|
|
expect(down(after)).toEqual(before)
|
|
|
|
expect(down(afterWithNonMatchingOpacity)).toEqual(before)
|
|
|
|
})
|
|
|
|
|
|
|
|
test('hoists opacity from propsForNextShape', () => {
|
|
|
|
const { up, down } = instanceMigrations.migrators[instanceTypeVersions.HoistOpacity]
|
|
|
|
const before = {
|
|
|
|
isToolLocked: true,
|
|
|
|
propsForNextShape: {
|
|
|
|
color: 'black',
|
|
|
|
opacity: '0.5',
|
|
|
|
},
|
|
|
|
}
|
|
|
|
const after = {
|
|
|
|
isToolLocked: true,
|
|
|
|
opacityForNextShape: 0.5,
|
|
|
|
propsForNextShape: {
|
|
|
|
color: 'black',
|
|
|
|
},
|
|
|
|
}
|
|
|
|
const afterWithNonMatchingOpacity = {
|
|
|
|
isToolLocked: true,
|
|
|
|
opacityForNextShape: 0.6,
|
|
|
|
propsForNextShape: {
|
|
|
|
color: 'black',
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
expect(up(before)).toEqual(after)
|
|
|
|
expect(down(after)).toEqual(before)
|
|
|
|
expect(down(afterWithNonMatchingOpacity)).toEqual(before)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2023-04-25 11:01:25 +00:00
|
|
|
/* --- PUT YOUR MIGRATIONS TESTS ABOVE HERE --- */
|
|
|
|
|
|
|
|
for (const migrator of allMigrators) {
|
|
|
|
test(`[${migrator.fileName} v${migrator.version}] up and down migrations have both been tested`, () => {
|
|
|
|
expect(migrator.up).toHaveBeenCalled()
|
|
|
|
expect(migrator.down).toHaveBeenCalled()
|
|
|
|
})
|
|
|
|
}
|