From d160e43eaea6bc76a4f833a374c61b41e09952cf Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Wed, 24 Aug 2022 21:46:09 +0530 Subject: [PATCH 01/16] Fix #968 : Ability to download and upload history --- cypress/e2e/history.spec.ts | 3 ++ src/lib/components/history/history.svelte | 50 +++++++++++++++++++++-- src/lib/components/history/history.ts | 5 +++ 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/cypress/e2e/history.spec.ts b/cypress/e2e/history.spec.ts index 92b558d8..851ea8b9 100644 --- a/cypress/e2e/history.spec.ts +++ b/cypress/e2e/history.spec.ts @@ -10,6 +10,9 @@ describe('Save History', () => { cy.contains('History').click(); }); + // TODO: Add test to verify state that's set in localstorage can be read. + // This is useful to know if migration of persistance layer will render old data invalid. + it('should save when clicked', () => { cy.get('#historyList').find('li').should('have.length', 0); cy.get('#historyList').contains('No items in History'); diff --git a/src/lib/components/history/history.svelte b/src/lib/components/history/history.svelte index 6ee60774..3e434fa9 100644 --- a/src/lib/components/history/history.svelte +++ b/src/lib/components/history/history.svelte @@ -7,10 +7,12 @@ clearHistoryData, getPreviousState, historyStore, - loaderHistoryStore + loaderHistoryStore, + restoreHistory } from './history'; import { notify, prompt } from '$lib/util/notify'; import { onMount } from 'svelte'; + import { get } from 'svelte/store'; import moment from 'moment'; import type { HistoryType, State, Tab } from '$lib/types'; @@ -32,6 +34,35 @@ } ]; + const downloadHistory = () => { + const data = get(historyStore); + const blob = new Blob([JSON.stringify(data)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'history.json'; + a.click(); + URL.revokeObjectURL(url); + }; + + const uploadHistory = () => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = 'application/json'; + input.addEventListener('change', ({ target }: Event) => { + const file = (target).files[0]; + if (!file) { + return; + } + const reader = new FileReader(); + reader.onload = (e) => { + const data = JSON.parse(e.target.result as string); + restoreHistory(data); + }; + reader.readAsText(file); + }); + input.click(); + }; const saveHistory = (auto = false) => { const currentState: string = getStateString(); const previousState: string = getPreviousState(auto); @@ -53,7 +84,7 @@ clearHistoryData(date); }; - const restoreHistory = (state: State): void => { + const restoreHistoryItem = (state: State): void => { inputStateStore.set({ ...state, updateEditor: true, updateDiagram: true }); }; @@ -88,6 +119,19 @@
+ + {#if $historyStore.length > 0} + + {/if} + |
- {#if type !== 'loader'}
    {#if $historyStore.length > 0} - {#each $historyStore as { state, time, name, url, type }} + {#each $historyStore as { id, state, time, name, url, type }}
  • @@ -168,7 +168,7 @@ {#if type !== 'loader'} - {/if}
    diff --git a/src/lib/components/history/history.ts b/src/lib/components/history/history.ts index ffdd81fa..afcc8574 100644 --- a/src/lib/components/history/history.ts +++ b/src/lib/components/history/history.ts @@ -2,7 +2,8 @@ import { derived, writable, get } from 'svelte/store'; import type { Readable, Writable } from 'svelte/store'; import { persist, localStorage } from '@macfja/svelte-persistent-store'; import { generateSlug } from 'random-word-slugs'; -import type { HistoryEntry, HistoryType } from '$lib/types'; +import type { HistoryEntry, HistoryType, Optional } from '$lib/types'; +import { v4 as uuidV4 } from 'uuid'; const MAX_AUTO_HISTORY_LENGTH = 30; @@ -41,28 +42,37 @@ export const historyStore: Readable = derived( } ); -export const addHistoryEntry = (entry: HistoryEntry): void => { +export const addHistoryEntry = (entryToAdd: Optional): void => { + const entry: HistoryEntry = { + ...entryToAdd, + id: uuidV4() + }; + if (entry.type === 'loader') { loaderHistoryStore.update((entries) => [entry, ...entries]); return; } - entry.name = generateSlug(2); - if (entry.type !== 'auto') { + + if (!entry.name) { + entry.name = generateSlug(2); + } + + if (entry.type === 'auto') { + autoHistoryStore.update((entries) => { + if (entries.length >= MAX_AUTO_HISTORY_LENGTH) { + entries = entries.slice(0, MAX_AUTO_HISTORY_LENGTH - 1); + } + return [entry, ...entries]; + }); + } else if (entry.type === 'manual') { manualHistoryStore.update((entries) => [entry, ...entries]); - return; } - autoHistoryStore.update((entries) => { - if (entries.length === MAX_AUTO_HISTORY_LENGTH) { - entries.pop(); - } - return [entry, ...entries]; - }); }; -export const clearHistoryData = (time?: number): void => { +export const clearHistoryData = (idToClear?: string): void => { (get(historyModeStore) === 'auto' ? autoHistoryStore : manualHistoryStore).update((entries) => { if (get(historyModeStore) !== 'loader') { - entries = entries.filter((entry) => time && entry.time != time); + entries = entries.filter(({ id }) => idToClear && id != idToClear); } return entries; }); @@ -77,6 +87,46 @@ export const getPreviousState = (auto: boolean): string => { }; export const restoreHistory = (data: HistoryEntry[]) => { - // Should this replace the current history or append to it? - manualHistoryStore.set(data); + const entries = data.filter(validateEntry); + const invalidEntryCount = data.length - entries.length; + if (invalidEntryCount > 0) { + console.error(`${invalidEntryCount} invalid history entries were removed.`); + console.error(data); + } + if (entries.length > 0) { + let entryCount = 0; + (entries[0].type === 'auto' ? autoHistoryStore : manualHistoryStore).update((existing) => { + const existingIDs = new Set(existing.map(({ id }) => id)); + const newEntries = entries.filter(({ id }) => !existingIDs.has(id)); + entryCount = newEntries.length; + const combined = [...existing, ...newEntries]; + combined.sort((a, b) => b.time - a.time); + return combined; + }); + + alert( + `${entryCount} entries restored. ${invalidEntryCount} invalid, ${ + entries.length - entryCount + } duplicates.` + ); + } else { + alert('No valid entries found.'); + } +}; + +export const injectHistoryIDs = (): void => { + const setIDs = (entries: HistoryEntry[]) => { + for (const entry of entries) { + if (!entry.id) { + entry.id = uuidV4(); + } + } + return entries; + }; + autoHistoryStore.update(setIDs); + manualHistoryStore.update(setIDs); +}; + +const validateEntry = (entry: HistoryEntry): boolean => { + return entry.type && entry.state && entry.time && true; }; diff --git a/src/lib/types.d.ts b/src/lib/types.d.ts index 1aaf954a..806d1f20 100644 --- a/src/lib/types.d.ts +++ b/src/lib/types.d.ts @@ -68,13 +68,16 @@ export interface LoaderConfig { config: GistLoaderConfig | FileLoaderConfig; } export type HistoryType = 'auto' | 'manual' | 'loader'; -export interface HistoryEntry { - state: State; - time: number; - name?: string; - type: HistoryType; - url?: string; -} +export type HistoryEntry = { id: string; state: State; time: number; url?: string } & ( + | { + type: 'loader'; + name: string; + } + | { + type: HistoryType; + name?: string; + } +); export interface DocConfig { [key: string]: { @@ -84,3 +87,4 @@ export interface DocConfig { } export type Loader = (url: string) => Promise; +export type Optional = Pick, K> & Omit; diff --git a/src/lib/util/migrations.ts b/src/lib/util/migrations.ts new file mode 100644 index 00000000..6b477746 --- /dev/null +++ b/src/lib/util/migrations.ts @@ -0,0 +1,32 @@ +import { writable, get, type Writable } from 'svelte/store'; +import { persist, localStorage } from '@macfja/svelte-persistent-store'; +import { injectHistoryIDs } from '$lib/components/history/history'; + +interface MigrationState { + version: number; +} + +const migrations: { [key: string]: () => void } = { + injectHistoryIDs +}; + +const migrationStore: Writable = persist( + writable({ version: -1 }), + localStorage(), + 'migrations' +); + +export const applyMigrations = (): void => { + const { version }: MigrationState = get(migrationStore); + const allMigrations = Object.entries(migrations); + if (version === allMigrations.length - 1) { + return; + } + console.log(`Current migration version: v${version}. Migrating to v${allMigrations.length - 1}.`); + for (let i = version + 1; i < allMigrations.length; i++) { + const [key, fn] = allMigrations[i]; + console.log(`Applying migration ${i}: ${key}.`); + fn(); + migrationStore.set({ version: i }); + } +}; diff --git a/src/routes/__layout.svelte b/src/routes/__layout.svelte index 26aa808c..097ea9fb 100644 --- a/src/routes/__layout.svelte +++ b/src/routes/__layout.svelte @@ -6,10 +6,12 @@ import { setTheme, themeStore } from '$lib/util/theme'; import { toggleDarkTheme } from '$lib/util/state'; import { initHandler } from '$lib/util/util'; + import { applyMigrations } from '$lib/util/migrations'; // This can be removed once https://github.com/sveltejs/kit/issues/1612 is fixed. // Then move it into src and vite will bundle it automatically. onMount(() => { + applyMigrations(); window.addEventListener('hashchange', async (ev) => { await initHandler(); }); diff --git a/yarn.lock b/yarn.lock index b3ef5f0a..3e916f66 100644 --- a/yarn.lock +++ b/yarn.lock @@ -450,6 +450,11 @@ dependencies: "@types/jest" "*" +"@types/uuid@^8.3.4": + version "8.3.4" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" + integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== + "@types/yauzl@^2.9.1": version "2.9.1" resolved "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz" From adc9dc7460312d00e1e347976f0cce4fda2ac250 Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Sun, 28 Aug 2022 22:55:47 +0530 Subject: [PATCH 03/16] Tests --- cypress/e2e/actions.spec.ts | 26 ++++++-------- cypress/e2e/history.spec.ts | 43 ++++++++++++++++++++--- cypress/e2e/util.ts | 32 +++++++++++++++++ cypress/snapshots.js | 2 +- src/lib/components/history/history.svelte | 2 +- 5 files changed, 83 insertions(+), 22 deletions(-) diff --git a/cypress/e2e/actions.spec.ts b/cypress/e2e/actions.spec.ts index 78e818e4..4de9d9b2 100644 --- a/cypress/e2e/actions.spec.ts +++ b/cypress/e2e/actions.spec.ts @@ -1,4 +1,4 @@ -import { disableDebounce } from './util'; +import { disableDebounce, verifyFileSize } from './util'; describe('Check actions', () => { beforeEach(() => { cy.clearLocalStorage(); @@ -27,28 +27,22 @@ describe('Check actions', () => { it('should download png and svg', () => { cy.clock(new Date(2022, 0, 1).getTime()); - const downloadsFolder = Cypress.config('downloadsFolder'); - const verifyFileSize = (fileType: string, size: number) => { - cy.get(`#download${fileType.toUpperCase()}`).click(); - const fileName = `mermaid-diagram-2022-01-01-000000.${fileType}`; - const filePath = `${downloadsFolder}/${fileName}`; - cy.verifyDownload(fileName); - cy.readFile(filePath, null, { - log: false - }).then((buffer) => expect((buffer as ArrayBuffer).byteLength).to.be.gt(size)); - cy.task('deleteFile', filePath); - }; + cy.get(`#downloadPNG`).click(); + verifyFileSize('diagram', 'png', 21_000); - verifyFileSize('png', 21_000); - verifyFileSize('svg', 11_000); + cy.get(`#downloadSVG`).click(); + verifyFileSize('diagram', 'svg', 10_000); // Verify downloaded file is different for different diagrams cy.contains('Sample Diagrams').click(); cy.contains('ER Diagram').click(); - verifyFileSize('png', 46_000); - verifyFileSize('svg', 12_000); + cy.get(`#downloadPNG`).click(); + verifyFileSize('diagram', 'png', 46_000); + + cy.get(`#downloadSVG`).click(); + verifyFileSize('diagram', 'svg', 12_000); cy.clock().invoke('restore'); }); diff --git a/cypress/e2e/history.spec.ts b/cypress/e2e/history.spec.ts index 851ea8b9..493ced6e 100644 --- a/cypress/e2e/history.spec.ts +++ b/cypress/e2e/history.spec.ts @@ -1,8 +1,8 @@ -import { getEditor, disableDebounce } from './util'; +import { getEditor, disableDebounce, verifyFileSnapshot } from './util'; describe('Save History', () => { beforeEach(() => { - cy.clock(); + cy.clock(new Date(2022, 0, 1).getTime()); cy.clearLocalStorage(); cy.visit('/edit'); disableDebounce(); @@ -10,8 +10,37 @@ describe('Save History', () => { cy.contains('History').click(); }); - // TODO: Add test to verify state that's set in localstorage can be read. - // This is useful to know if migration of persistance layer will render old data invalid. + afterEach(() => { + cy.clock().invoke('restore'); + }); + + it('should load history from localstorage', () => { + cy.setLocalStorage( + 'manualHistoryStore', + '[{"state":{"code":"graph TD\\n A[Halloween] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","updateEditor":false,"autoSync":true,"updateDiagram":false},"time":0,"type":"manual","id":"d7ea820e-21dd-418a-b984-fd58acde09df","name":"hollow-art"},{"state":{"code":"graph TD\\n A[Christmas] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","updateEditor":true,"autoSync":true,"updateDiagram":true},"time":0,"type":"manual","id":"b749ffc6-522b-4a44-86cf-7c1ffc3146b3","name":"helpful-ocean"}]' + ); + cy.setLocalStorage( + 'autoHistoryStore', + '[{"state":{"code":"graph TD\\n A[New Year] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","updateEditor":false,"autoSync":true,"updateDiagram":false},"time":0,"type":"auto","id":"69ea820e-522b-4a44-86cf-fd58acde09df","name":"barking-dog"},{"state":{"code":"graph TD\\n A[Christmas] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","updateEditor":true,"autoSync":true,"updateDiagram":true},"time":0,"type":"manual","id":"x749ffc6-21dd-418a-b984-7c1ffc3146b3","name":"needy-mosquito"}]' + ); + cy.reload(); + cy.contains('Actions').click(); + cy.contains('History').click(); + cy.get('#historyList').find('li').should('have.length', 2); + cy.get('#historyList').find('No items in History').should('not.exist'); + cy.get('#historyList').contains('helpful-ocean'); + cy.get('#historyList').contains('hollow-art'); + cy.contains('Restore').click(); + cy.contains('Halloween'); + cy.contains('Timeline').click(); + + cy.get('#historyList').find('li').should('have.length', 2); + cy.get('#historyList').find('No items in History').should('not.exist'); + cy.get('#historyList').contains('needy-mosquito'); + cy.get('#historyList').contains('barking-dog'); + cy.contains('Restore').click(); + cy.contains('New Year'); + }); it('should save when clicked', () => { cy.get('#historyList').find('li').should('have.length', 0); @@ -67,4 +96,10 @@ describe('Save History', () => { } cy.get('#historyList').find('li').should('have.length', 30); }); + + it('should download history', () => { + cy.get('#saveHistory').click(); + cy.get(`#downloadHistory`).click(); + verifyFileSnapshot('history', 'json', 'A[Christmas] -->|Get money| B(Go shopping)'); + }); }); diff --git a/cypress/e2e/util.ts b/cypress/e2e/util.ts index e715fd36..46416037 100644 --- a/cypress/e2e/util.ts +++ b/cypress/e2e/util.ts @@ -9,3 +9,35 @@ export const getEditor = ({ bottom = true, newline = false } = {}) => .type(`${newline ? '{enter}' : cmd}`); export const disableDebounce = () => cy.setLocalStorage('noDebounce', 'true'); + +const downloadsFolder = Cypress.config('downloadsFolder'); + +export const verifyFileSize = ( + fileType: 'history' | 'diagram', + extension: string, + size: number +) => { + const fileName = `mermaid-${fileType}-2022-01-01-000000.${extension}`; + const filePath = `${downloadsFolder}/${fileName}`; + cy.verifyDownload(fileName); + cy.readFile(filePath, null, { + log: false + }).then((buffer) => expect((buffer as ArrayBuffer).byteLength).to.be.gt(size)); + cy.task('deleteFile', filePath); +}; + +export const verifyFileSnapshot = ( + fileType: 'history' | 'diagram', + extension: string, + content: string +) => { + const fileName = `mermaid-${fileType}-2022-01-01-000000.${extension}`; + const filePath = `${downloadsFolder}/${fileName}`; + cy.verifyDownload(fileName); + cy.readFile(filePath, null, { + log: false + }).then((buffer) => + expect(new TextDecoder('utf-8').decode(buffer as ArrayBuffer)).to.contain(content) + ); + cy.task('deleteFile', filePath); +}; diff --git a/cypress/snapshots.js b/cypress/snapshots.js index 0de6a809..72b46c0b 100644 --- a/cypress/snapshots.js +++ b/cypress/snapshots.js @@ -16,7 +16,7 @@ module.exports = { "1": "{\"code\":\"graph TD\\n A[Party] -->|Get money| B(Go shopping!!)\\n \",\"mermaid\":\"{\\n \\\"theme\\\": \\\"forest\\\",\\n \\\"test\\\": \\\"hello world\\\"\\n}\",\"updateEditor\":false,\"autoSync\":true,\"updateDiagram\":true,\"loader\":{\"type\":\"files\",\"config\":{\"codeURL\":\"https://gist.githubusercontent.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/raw/4eb03887e6a41397e80bdcdbf94017c498f8f1e2/code.mmd\",\"configURL\":\"https://gist.githubusercontent.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/raw/4eb03887e6a41397e80bdcdbf94017c498f8f1e2/config.json\"}}}" } }, - "__version": "10.4.0", + "__version": "10.6.0", "Auto sync tests": { "should dim diagram when code is edited": { "1": "{\"code\":\"graph TD\\n A[Christmas] -->|Get money| B(Go shopping)\\n B --> C{Let me think}\\n C -->|One| D[Laptop]\\n C -->|Two| E[iPhone]\\n C -->|Three| F[fa:fa-car Car]\\n C --> Test\",\"mermaid\":\"{\\n \\\"theme\\\": \\\"default\\\"\\n}\",\"updateEditor\":false,\"autoSync\":false,\"updateDiagram\":false}" diff --git a/src/lib/components/history/history.svelte b/src/lib/components/history/history.svelte index 525fa3e6..efc1fa9d 100644 --- a/src/lib/components/history/history.svelte +++ b/src/lib/components/history/history.svelte @@ -40,7 +40,7 @@ const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; - a.download = 'history.json'; + a.download = `mermaid-history-${moment().format('YYYY-MM-DD-HHmmss')}.json`; a.click(); URL.revokeObjectURL(url); }; From 04b898ee5441353ff3cde3a6f3f97dee26153b37 Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Sun, 28 Aug 2022 23:10:40 +0530 Subject: [PATCH 04/16] Metrics --- src/lib/components/history/history.svelte | 5 +++++ src/lib/components/history/history.ts | 9 +++++++++ src/lib/util/migrations.ts | 3 +++ 3 files changed, 17 insertions(+) diff --git a/src/lib/components/history/history.svelte b/src/lib/components/history/history.svelte index efc1fa9d..330332af 100644 --- a/src/lib/components/history/history.svelte +++ b/src/lib/components/history/history.svelte @@ -15,6 +15,7 @@ import { get } from 'svelte/store'; import moment from 'moment'; import type { HistoryType, State, Tab } from '$lib/types'; + import { logEvent } from '$lib/util/stats'; const HISTORY_SAVE_INTERVAL = 60000; @@ -43,6 +44,9 @@ a.download = `mermaid-history-${moment().format('YYYY-MM-DD-HHmmss')}.json`; a.click(); URL.revokeObjectURL(url); + logEvent('history', { + action: 'download' + }); }; const uploadHistory = () => { @@ -63,6 +67,7 @@ }); input.click(); }; + const saveHistory = (auto = false) => { const currentState: string = getStateString(); const previousState: string = getPreviousState(auto); diff --git a/src/lib/components/history/history.ts b/src/lib/components/history/history.ts index afcc8574..923a1d2e 100644 --- a/src/lib/components/history/history.ts +++ b/src/lib/components/history/history.ts @@ -4,6 +4,7 @@ import { persist, localStorage } from '@macfja/svelte-persistent-store'; import { generateSlug } from 'random-word-slugs'; import type { HistoryEntry, HistoryType, Optional } from '$lib/types'; import { v4 as uuidV4 } from 'uuid'; +import { logEvent } from '$lib/util/stats'; const MAX_AUTO_HISTORY_LENGTH = 30; @@ -66,6 +67,7 @@ export const addHistoryEntry = (entryToAdd: Optional): void }); } else if (entry.type === 'manual') { manualHistoryStore.update((entries) => [entry, ...entries]); + logEvent('history', { action: 'save' }); } }; @@ -73,6 +75,7 @@ export const clearHistoryData = (idToClear?: string): void => { (get(historyModeStore) === 'auto' ? autoHistoryStore : manualHistoryStore).update((entries) => { if (get(historyModeStore) !== 'loader') { entries = entries.filter(({ id }) => idToClear && id != idToClear); + logEvent('history', { action: 'clear', type: idToClear ? 'single' : 'all' }); } return entries; }); @@ -109,6 +112,12 @@ export const restoreHistory = (data: HistoryEntry[]) => { entries.length - entryCount } duplicates.` ); + logEvent('history', { + action: 'restore', + success: entryCount, + invalid: invalidEntryCount, + duplicates: entries.length - entryCount + }); } else { alert('No valid entries found.'); } diff --git a/src/lib/util/migrations.ts b/src/lib/util/migrations.ts index 6b477746..40b6a1bc 100644 --- a/src/lib/util/migrations.ts +++ b/src/lib/util/migrations.ts @@ -1,6 +1,7 @@ import { writable, get, type Writable } from 'svelte/store'; import { persist, localStorage } from '@macfja/svelte-persistent-store'; import { injectHistoryIDs } from '$lib/components/history/history'; +import { logEvent } from './stats'; interface MigrationState { version: number; @@ -27,6 +28,8 @@ export const applyMigrations = (): void => { const [key, fn] = allMigrations[i]; console.log(`Applying migration ${i}: ${key}.`); fn(); + logEvent('migration', { key }); migrationStore.set({ version: i }); } + logEvent('migration', { status: 'complete', from: version, to: allMigrations.length - 1 }); }; From d966b231148d76a9700b13b421d521c1b4f7650b Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Mon, 29 Aug 2022 21:56:26 +0530 Subject: [PATCH 05/16] Unit tests for history --- .vscode/settings.json | 5 +- src/lib/components/history/history.test.ts | 119 +++++++++++++++++++++ src/lib/util/migrations.test.ts | 34 ++++++ 3 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 src/lib/components/history/history.test.ts create mode 100644 src/lib/util/migrations.test.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index cd3023f6..1403e65a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,7 @@ { "editor.formatOnSave": true, - "cSpell.words": ["pako", "Serde", "serdes"] + "cSpell.words": ["pako", "Serde", "serdes"], + "vitest.commandLine": "yarn test:unit", + "vitest.enable": true, + "testing.autoRun.mode": "rerun" } diff --git a/src/lib/components/history/history.test.ts b/src/lib/components/history/history.test.ts new file mode 100644 index 00000000..3fbca5a5 --- /dev/null +++ b/src/lib/components/history/history.test.ts @@ -0,0 +1,119 @@ +import type { HistoryEntry } from '$lib/types'; +import { describe, it, expect, beforeEach } from 'vitest'; +import { + addHistoryEntry, + injectHistoryIDs, + clearHistoryData, + historyModeStore, + historyStore +} from './history'; +import { defaultState } from '../../util/state'; +import { get } from 'svelte/store'; + +describe('history', () => { + it('should handle saving individual history entry', () => { + expect(window.localStorage.getItem('manualHistoryStore')).toBe('[]'); + expect(window.localStorage.getItem('autoHistoryStore')).toBe('[]'); + + addHistoryEntry({ + state: defaultState, + time: 12345, + type: 'manual' + }); + + const [manualEntry]: HistoryEntry[] = JSON.parse( + window.localStorage.getItem('manualHistoryStore') + ); + + expect(manualEntry.time).toBe(12345); + expect(manualEntry.type).toBe('manual'); + expect(manualEntry.name).not.toBeNull(); + expect(manualEntry.state).not.toBeNull(); + + addHistoryEntry({ + state: defaultState, + time: 54321, + type: 'auto' + }); + + const [autoEntry]: HistoryEntry[] = JSON.parse(window.localStorage.getItem('autoHistoryStore')); + + expect(autoEntry.time).toBe(54321); + expect(autoEntry.type).toBe('auto'); + expect(autoEntry.name).not.toBeNull(); + expect(autoEntry.state).not.toBeNull(); + + historyModeStore.set('manual'); + clearHistoryData(); + historyModeStore.set('auto'); + clearHistoryData(); + expect(window.localStorage.getItem('manualHistoryStore')).toBe('[]'); + expect(window.localStorage.getItem('autoHistoryStore')).toBe('[]'); + }); + + it('should clear history entries', () => { + addHistoryEntry({ + state: defaultState, + time: 12345, + type: 'manual' + }); + addHistoryEntry({ + state: { ...defaultState, code: 'graph TD\\n A[Christmas] -->|Get money| B(Go shopping)' }, + time: 123456, + type: 'manual' + }); + addHistoryEntry({ + state: defaultState, + time: 54321, + type: 'auto' + }); + addHistoryEntry({ + state: { ...defaultState, code: 'graph TD\\n A[Christmas] -->|Get money| B(Go shopping)' }, + time: 654321, + type: 'auto' + }); + + const store: HistoryEntry[] = get(historyStore); + expect(store.length).toBe(2); + clearHistoryData(store[1].id); + expect(get(historyStore).length).toBe(1); + clearHistoryData(); + expect(get(historyStore).length).toBe(0); + + historyModeStore.set('auto'); + expect(get(historyStore).length).toBe(2); + clearHistoryData(); + expect(get(historyStore).length).toBe(0); + // Test calling when history is empty + clearHistoryData(); + expect(get(historyStore).length).toBe(0); + }); +}); + +describe('history migration', () => { + it('should inject history IDs as migration', () => { + window.localStorage.setItem( + 'manualHistoryStore', + '[{"state":{"code":"graph TD\\n A[Halloween] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","updateEditor":false,"autoSync":true,"updateDiagram":false},"time":0,"type":"manual","name":"hollow-art"},{"state":{"code":"graph TD\\n A[Christmas] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","updateEditor":true,"autoSync":true,"updateDiagram":true},"time":0,"type":"manual","name":"helpful-ocean"}]' + ); + window.localStorage.setItem( + 'autoHistoryStore', + '[{"state":{"code":"graph TD\\n A[New Year] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","updateEditor":false,"autoSync":true,"updateDiagram":false},"time":0,"type":"auto","name":"barking-dog"},{"state":{"code":"graph TD\\n A[Christmas] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","updateEditor":true,"autoSync":true,"updateDiagram":true},"time":0,"type":"manual","name":"needy-mosquito"}]' + ); + let manualHistoryStore: HistoryEntry[] = JSON.parse( + window.localStorage.getItem('manualHistoryStore') + ); + let autoHistoryStore: HistoryEntry[] = JSON.parse( + window.localStorage.getItem('autoHistoryStore') + ); + expect(manualHistoryStore.every(({ id }) => id !== undefined)).toBe(false); + expect(autoHistoryStore.every(({ id }) => id !== undefined)).toBe(false); + + injectHistoryIDs(); + + manualHistoryStore = JSON.parse(window.localStorage.getItem('manualHistoryStore')); + autoHistoryStore = JSON.parse(window.localStorage.getItem('autoHistoryStore')); + expect(manualHistoryStore.every(({ id }) => id !== undefined)).toBe(true); + expect(autoHistoryStore.every(({ id }) => id !== undefined)).toBe(true); + }); +}); diff --git a/src/lib/util/migrations.test.ts b/src/lib/util/migrations.test.ts new file mode 100644 index 00000000..9702c658 --- /dev/null +++ b/src/lib/util/migrations.test.ts @@ -0,0 +1,34 @@ +import type { HistoryEntry } from '$lib/types'; +import { describe, it, expect, beforeEach } from 'vitest'; + +describe('migrations', () => { + beforeEach(() => { + window.localStorage.setItem( + 'manualHistoryStore', + '[{"state":{"code":"graph TD\\n A[Halloween] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","updateEditor":false,"autoSync":true,"updateDiagram":false},"time":0,"type":"manual","name":"hollow-art"},{"state":{"code":"graph TD\\n A[Christmas] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","updateEditor":true,"autoSync":true,"updateDiagram":true},"time":0,"type":"manual","name":"helpful-ocean"}]' + ); + window.localStorage.setItem( + 'autoHistoryStore', + '[{"state":{"code":"graph TD\\n A[New Year] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","updateEditor":false,"autoSync":true,"updateDiagram":false},"time":0,"type":"auto","name":"barking-dog"},{"state":{"code":"graph TD\\n A[Christmas] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","updateEditor":true,"autoSync":true,"updateDiagram":true},"time":0,"type":"manual","name":"needy-mosquito"}]' + ); + }); + + it('should migrate from v0 to v1', async () => { + const { applyMigrations } = await import('./migrations'); + let manualHistoryStore: HistoryEntry[] = JSON.parse( + window.localStorage.getItem('manualHistoryStore') + ); + let autoHistoryStore: HistoryEntry[] = JSON.parse( + window.localStorage.getItem('autoHistoryStore') + ); + expect(manualHistoryStore.every(({ id }) => id !== undefined)).toBe(false); + expect(autoHistoryStore.every(({ id }) => id !== undefined)).toBe(false); + + applyMigrations(); + + manualHistoryStore = JSON.parse(window.localStorage.getItem('manualHistoryStore')); + autoHistoryStore = JSON.parse(window.localStorage.getItem('autoHistoryStore')); + expect(manualHistoryStore.every(({ id }) => id !== undefined)).toBe(true); + expect(autoHistoryStore.every(({ id }) => id !== undefined)).toBe(true); + }); +}); From de3ce94bb47084a7ee44b90d25550fcbe932ba46 Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Tue, 30 Aug 2022 08:35:36 +0530 Subject: [PATCH 06/16] Unit tests for history --- src/lib/components/history/history.test.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/lib/components/history/history.test.ts b/src/lib/components/history/history.test.ts index 3fbca5a5..2f0e49eb 100644 --- a/src/lib/components/history/history.test.ts +++ b/src/lib/components/history/history.test.ts @@ -62,6 +62,15 @@ describe('history', () => { time: 123456, type: 'manual' }); + + const store: HistoryEntry[] = get(historyStore); + expect(store.length).toBe(2); + clearHistoryData(store[1].id); + expect(get(historyStore).length).toBe(1); + clearHistoryData(); + expect(get(historyStore).length).toBe(0); + + historyModeStore.set('auto'); addHistoryEntry({ state: defaultState, time: 54321, @@ -72,15 +81,6 @@ describe('history', () => { time: 654321, type: 'auto' }); - - const store: HistoryEntry[] = get(historyStore); - expect(store.length).toBe(2); - clearHistoryData(store[1].id); - expect(get(historyStore).length).toBe(1); - clearHistoryData(); - expect(get(historyStore).length).toBe(0); - - historyModeStore.set('auto'); expect(get(historyStore).length).toBe(2); clearHistoryData(); expect(get(historyStore).length).toBe(0); From 5cb681c76611108adb11beede5e6375edfff2f4a Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Tue, 30 Aug 2022 08:44:09 +0530 Subject: [PATCH 07/16] SvelteKit 453 --- package.json | 7 +++--- src/lib/components/actions.svelte | 2 +- src/lib/util/stats.ts | 2 +- src/tests/setup.ts | 12 +++++----- yarn.lock | 39 +++++++++++++++++++++++-------- 5 files changed, 40 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index 279fae06..f9c945e0 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "devDependencies": { "@cypress/snapshot": "2.1.7", "@sveltejs/adapter-static": "1.0.0-next.39", - "@sveltejs/kit": "1.0.0-next.442", + "@sveltejs/kit": "1.0.0-next.453", "@testing-library/jest-dom": "5.16.5", "@testing-library/svelte": "3.2.1", "@types/mermaid": "8.2.9", @@ -58,9 +58,8 @@ "tailwindcss": "3.1.8", "tslib": "2.4.0", "typescript": "4.8.2", - "vite": "3.0.9", - "vitest": "0.22.1", - "vitest-svelte-kit": "0.0.7" + "vite": "3.1.0-beta.1", + "vitest": "0.22.1" }, "dependencies": { "@analytics/google-analytics": "1.0.3", diff --git a/src/lib/components/actions.svelte b/src/lib/components/actions.svelte index 8e75a3f2..56476363 100644 --- a/src/lib/components/actions.svelte +++ b/src/lib/components/actions.svelte @@ -1,5 +1,5 @@