@sutrata/editor 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. package/README.md +87 -0
  2. package/dist/ai/VoiceConfirmDialog.d.ts +1 -0
  3. package/dist/ai/VoiceConfirmDialog.js +6 -6
  4. package/dist/ai/VoiceToolbar.d.ts +1 -0
  5. package/dist/ai/VoiceToolbar.js +22 -16
  6. package/dist/ai/clean-response.d.ts +7 -0
  7. package/dist/ai/clean-response.js +58 -0
  8. package/dist/ai/format-transcript.d.ts +3 -0
  9. package/dist/ai/format-transcript.js +7 -0
  10. package/dist/ai/prompts.d.ts +8 -1
  11. package/dist/ai/prompts.js +34 -7
  12. package/dist/autocomplete/suggestions.js +1 -1
  13. package/dist/context/DocumentContext.d.ts +28 -4
  14. package/dist/context/DocumentContext.js +67 -37
  15. package/dist/editor/EditorView.js +32 -4
  16. package/dist/editor/SelectionMenu.js +5 -3
  17. package/dist/editor/external-decorations-plugin.d.ts +23 -0
  18. package/dist/editor/external-decorations-plugin.js +151 -0
  19. package/dist/editor/omitted-scene-plugin.d.ts +9 -0
  20. package/dist/editor/omitted-scene-plugin.js +44 -0
  21. package/dist/editor/plugins.js +4 -0
  22. package/dist/editor/private-metadata-plugin.d.ts +10 -0
  23. package/dist/editor/private-metadata-plugin.js +60 -0
  24. package/dist/editor/prosemirror-to-sutra.js +179 -74
  25. package/dist/editor/scene-heading-view.js +2 -0
  26. package/dist/editor/schema.js +69 -12
  27. package/dist/editor/sutra-to-prosemirror.d.ts +2 -0
  28. package/dist/editor/sutra-to-prosemirror.js +32 -34
  29. package/dist/extensions/ai-provider.d.ts +44 -0
  30. package/dist/extensions/ai-provider.js +27 -0
  31. package/dist/extensions/collab.d.ts +21 -0
  32. package/dist/extensions/collab.js +6 -0
  33. package/dist/extensions/command-registry.d.ts +49 -0
  34. package/dist/extensions/command-registry.js +28 -0
  35. package/dist/extensions/decorations.d.ts +48 -0
  36. package/dist/extensions/decorations.js +6 -0
  37. package/dist/extensions/export-registry.d.ts +59 -0
  38. package/dist/extensions/export-registry.js +10 -0
  39. package/dist/extensions/panel-registry.d.ts +30 -0
  40. package/dist/extensions/panel-registry.js +16 -0
  41. package/dist/extensions/registries.d.ts +15 -0
  42. package/dist/extensions/registries.js +12 -0
  43. package/dist/extensions/registry-store.d.ts +26 -0
  44. package/dist/extensions/registry-store.js +35 -0
  45. package/dist/extensions/session.d.ts +29 -0
  46. package/dist/extensions/session.js +20 -0
  47. package/dist/extensions/speech-provider.d.ts +35 -0
  48. package/dist/extensions/speech-provider.js +17 -0
  49. package/dist/file/builtin-exporters.d.ts +9 -0
  50. package/dist/file/builtin-exporters.js +72 -0
  51. package/dist/file/docx-exporter.js +7 -4
  52. package/dist/file/docx-importer.d.ts +9 -0
  53. package/dist/file/docx-importer.js +47 -19
  54. package/dist/file/scene-number.d.ts +5 -0
  55. package/dist/file/scene-number.js +8 -0
  56. package/dist/file/workflow-reports.js +13 -7
  57. package/dist/findreplace/FindReplace.js +5 -2
  58. package/dist/import/AIImportDialog.d.ts +11 -0
  59. package/dist/import/AIImportDialog.js +112 -0
  60. package/dist/import/ai-import.d.ts +51 -0
  61. package/dist/import/ai-import.js +200 -0
  62. package/dist/import/apply-import.d.ts +7 -0
  63. package/dist/import/apply-import.js +35 -0
  64. package/dist/import/extract.d.ts +17 -0
  65. package/dist/import/extract.js +18 -0
  66. package/dist/index.d.ts +17 -2
  67. package/dist/index.js +7 -0
  68. package/dist/locales/bn.json +8 -0
  69. package/dist/locales/en.json +8 -0
  70. package/dist/locales/gu.json +8 -0
  71. package/dist/locales/hi.json +8 -0
  72. package/dist/locales/kn.json +8 -0
  73. package/dist/locales/ml.json +8 -0
  74. package/dist/locales/mr.json +8 -0
  75. package/dist/locales/or.json +8 -0
  76. package/dist/locales/pa.json +8 -0
  77. package/dist/locales/si.json +8 -0
  78. package/dist/locales/ta.json +8 -0
  79. package/dist/locales/te.json +8 -0
  80. package/dist/navigator/SceneNavigator.js +125 -131
  81. package/dist/navigator/StatisticsDialog.js +5 -1
  82. package/dist/navigator/scene-edits.d.ts +18 -0
  83. package/dist/navigator/scene-edits.js +143 -0
  84. package/dist/navigator/scene-list.js +4 -4
  85. package/dist/navigator/scene-numbering.d.ts +37 -0
  86. package/dist/navigator/scene-numbering.js +88 -0
  87. package/dist/settings/SettingsDialog.js +8 -120
  88. package/dist/shell/AppBar.js +21 -2
  89. package/dist/shell/AppShell.js +18 -4
  90. package/dist/shell/ElementToolbar.d.ts +5 -0
  91. package/dist/shell/ElementToolbar.js +35 -25
  92. package/dist/shell/LanguageStatus.js +6 -0
  93. package/dist/shell/StatusBar.js +11 -6
  94. package/dist/shell/builtin-commands.d.ts +10 -0
  95. package/dist/shell/builtin-commands.js +43 -0
  96. package/dist/shell/icons.d.ts +6 -0
  97. package/dist/shell/icons.js +6 -0
  98. package/dist/shell/use-commands.d.ts +7 -0
  99. package/dist/shell/use-commands.js +24 -0
  100. package/dist/shell/use-panel-context.d.ts +3 -0
  101. package/dist/shell/use-panel-context.js +39 -0
  102. package/dist/source/SourceView.js +18 -1
  103. package/dist/styles/screenplay.css +113 -1
  104. package/dist/titlepage/ExportDialog.d.ts +5 -0
  105. package/dist/titlepage/ExportDialog.js +59 -96
  106. package/package.json +32 -5
  107. package/dist/ai/OnboardingDialog.d.ts +0 -6
  108. package/dist/ai/OnboardingDialog.js +0 -115
  109. package/dist/ai/ai-client.d.ts +0 -71
  110. package/dist/ai/ai-client.js +0 -494
  111. package/dist/ai/generated-providers.json +0 -3696
  112. package/dist/ai/voice-service.d.ts +0 -15
  113. package/dist/ai/voice-service.js +0 -81
  114. package/dist/editor/character-view.d.ts +0 -35
  115. package/dist/editor/character-view.js +0 -112
  116. package/dist/editor/transliterate-input.d.ts +0 -10
  117. package/dist/editor/transliterate-input.js +0 -88
  118. package/dist/extensions/types.d.ts +0 -73
  119. package/dist/extensions/types.js +0 -12
  120. package/scripts/copy-assets.mjs +0 -16
  121. package/scripts/generate-providers.mjs +0 -127
  122. package/src/ai/OnboardingDialog.tsx +0 -390
  123. package/src/ai/VoiceConfirmDialog.tsx +0 -137
  124. package/src/ai/VoiceToolbar.tsx +0 -321
  125. package/src/ai/ai-client.ts +0 -596
  126. package/src/ai/commands.ts +0 -164
  127. package/src/ai/generated-providers.json +0 -3696
  128. package/src/ai/prompts.ts +0 -62
  129. package/src/ai/voice-service.ts +0 -96
  130. package/src/autocomplete/AutocompletePlugin.ts +0 -21
  131. package/src/autocomplete/suggestions.ts +0 -147
  132. package/src/context/DocumentContext.tsx +0 -648
  133. package/src/desktop-api.d.ts +0 -40
  134. package/src/editor/EditorView.tsx +0 -82
  135. package/src/editor/SelectionMenu.tsx +0 -152
  136. package/src/editor/attr-pencil-plugin.ts +0 -172
  137. package/src/editor/character-decoration-plugin.ts +0 -104
  138. package/src/editor/editor-bus.ts +0 -51
  139. package/src/editor/find-highlight-plugin.ts +0 -68
  140. package/src/editor/frontmatter-field-view.ts +0 -52
  141. package/src/editor/frontmatter-field.ts +0 -115
  142. package/src/editor/grapheme-cursor.ts +0 -99
  143. package/src/editor/ime-plugin.ts +0 -85
  144. package/src/editor/insert-helper.ts +0 -43
  145. package/src/editor/keymap.ts +0 -124
  146. package/src/editor/language-class-plugin.ts +0 -91
  147. package/src/editor/plugins.ts +0 -26
  148. package/src/editor/prosemirror-to-sutra.ts +0 -192
  149. package/src/editor/scene-heading-view.ts +0 -147
  150. package/src/editor/schema.ts +0 -156
  151. package/src/editor/sutra-to-prosemirror.ts +0 -156
  152. package/src/extensions/storage-adapter.ts +0 -65
  153. package/src/extensions/types.ts +0 -60
  154. package/src/file/docx-exporter.ts +0 -585
  155. package/src/file/docx-importer.ts +0 -313
  156. package/src/file/print-paginator.ts +0 -301
  157. package/src/file/templates.ts +0 -73
  158. package/src/file/workflow-reports.ts +0 -820
  159. package/src/findreplace/FindReplace.tsx +0 -242
  160. package/src/findreplace/find-engine.ts +0 -97
  161. package/src/findreplace/scroll-to-match.ts +0 -89
  162. package/src/fonts/tofu-detect.ts +0 -53
  163. package/src/i18n/LanguageContext.tsx +0 -207
  164. package/src/i18n/language-detect.ts +0 -73
  165. package/src/i18n/useTranslation.tsx +0 -74
  166. package/src/index.ts +0 -27
  167. package/src/locales/bn.json +0 -143
  168. package/src/locales/en.json +0 -143
  169. package/src/locales/gu.json +0 -143
  170. package/src/locales/hi.json +0 -143
  171. package/src/locales/kn.json +0 -143
  172. package/src/locales/ml.json +0 -143
  173. package/src/locales/mr.json +0 -143
  174. package/src/locales/or.json +0 -143
  175. package/src/locales/pa.json +0 -143
  176. package/src/locales/si.json +0 -143
  177. package/src/locales/ta.json +0 -143
  178. package/src/locales/te.json +0 -143
  179. package/src/navigator/SceneNavigator.tsx +0 -844
  180. package/src/navigator/StatisticsDialog.tsx +0 -249
  181. package/src/navigator/scene-list.ts +0 -84
  182. package/src/settings/SettingsDialog.tsx +0 -303
  183. package/src/shell/AppBar.tsx +0 -354
  184. package/src/shell/AppShell.tsx +0 -71
  185. package/src/shell/ConfirmDialog.tsx +0 -71
  186. package/src/shell/ElementToolbar.tsx +0 -175
  187. package/src/shell/LanguageStatus.tsx +0 -101
  188. package/src/shell/StatusBar.tsx +0 -135
  189. package/src/shell/SutrataLogo.tsx +0 -89
  190. package/src/shell/Toast.tsx +0 -46
  191. package/src/shell/ToolbarButton.tsx +0 -30
  192. package/src/shell/VersionHistory.tsx +0 -212
  193. package/src/shell/Watermark.tsx +0 -9
  194. package/src/shell/fonts/fonts.css +0 -1133
  195. package/src/shell/icons.tsx +0 -335
  196. package/src/shell/toolbar-actions.ts +0 -59
  197. package/src/source/SourceView.tsx +0 -111
  198. package/src/source/find-highlight-source.ts +0 -57
  199. package/src/source/sutra-lang.ts +0 -121
  200. package/src/spellcheck/dictionaries.ts +0 -21
  201. package/src/spellcheck/hunspell.ts +0 -110
  202. package/src/spellcheck/spellcheck-plugin.ts +0 -83
  203. package/src/styles/StyleDialog.tsx +0 -148
  204. package/src/styles/builtin-styles.ts +0 -139
  205. package/src/styles/css-adapter.ts +0 -76
  206. package/src/styles/docx-adapter.ts +0 -109
  207. package/src/styles/embed-fonts.ts +0 -81
  208. package/src/styles/pagination.ts +0 -27
  209. package/src/styles/print-adapter.ts +0 -288
  210. package/src/styles/registry.ts +0 -20
  211. package/src/styles/screenplay.css +0 -3006
  212. package/src/styles/script-ranges.ts +0 -17
  213. package/src/styles/types.ts +0 -104
  214. package/src/styles/validate.ts +0 -114
  215. package/src/templates/templates.ts +0 -194
  216. package/src/titlepage/ExportDialog.tsx +0 -494
  217. package/src/titlepage/TitlePageDialog.tsx +0 -312
  218. package/src/titlepage/frontmatter-form.ts +0 -280
  219. package/src/titlepage/languages.ts +0 -37
  220. package/src/types.ts +0 -7
  221. package/tests/AppBar.test.tsx +0 -35
  222. package/tests/AppShell.test.tsx +0 -22
  223. package/tests/ElementToolbar.test.tsx +0 -36
  224. package/tests/LanguageStatus.test.tsx +0 -129
  225. package/tests/SelectionMenu.test.tsx +0 -71
  226. package/tests/ToolbarButton.test.tsx +0 -24
  227. package/tests/ai-onboarding.test.ts +0 -102
  228. package/tests/ai-status.test.tsx +0 -72
  229. package/tests/ai-structured.test.ts +0 -94
  230. package/tests/autocomplete/AutocompletePlugin.test.ts +0 -20
  231. package/tests/autocomplete/suggestions.test.ts +0 -106
  232. package/tests/document-context.test.tsx +0 -565
  233. package/tests/docx-importer.test.ts +0 -160
  234. package/tests/editor/character-decoration-plugin.test.ts +0 -101
  235. package/tests/editor/character-extension.test.ts +0 -44
  236. package/tests/editor/grapheme-cursor.test.ts +0 -118
  237. package/tests/editor/ime-plugin.test.ts +0 -72
  238. package/tests/editor/inline-note-bridge.test.ts +0 -32
  239. package/tests/editor/language-class-plugin.test.ts +0 -265
  240. package/tests/editor/title-page.test.ts +0 -124
  241. package/tests/embed-fonts.test.ts +0 -17
  242. package/tests/exporters.test.ts +0 -365
  243. package/tests/findreplace/FindReplace.test.tsx +0 -391
  244. package/tests/findreplace/find-engine.test.ts +0 -86
  245. package/tests/findreplace/scroll-to-match.test.ts +0 -50
  246. package/tests/fonts/tofu-detect.test.ts +0 -122
  247. package/tests/i18n/LanguageContext.test.tsx +0 -146
  248. package/tests/i18n/language-detect.test.ts +0 -54
  249. package/tests/i18n/locale-parity.test.ts +0 -25
  250. package/tests/i18n/useTranslation.test.tsx +0 -45
  251. package/tests/keymap.test.ts +0 -18
  252. package/tests/navigator/SceneNavigator.test.tsx +0 -177
  253. package/tests/navigator/StatisticsDialog.test.tsx +0 -63
  254. package/tests/navigator/scene-list.test.ts +0 -71
  255. package/tests/print-paginator.test.ts +0 -82
  256. package/tests/shell/VersionHistory.test.tsx +0 -129
  257. package/tests/spellcheck/hunspell.test.ts +0 -82
  258. package/tests/spellcheck/spellcheck-plugin.test.ts +0 -56
  259. package/tests/styles.test.ts +0 -315
  260. package/tests/templates/templates.test.ts +0 -83
  261. package/tests/titlepage/TitlePageDialog.test.tsx +0 -90
  262. package/tests/titlepage/apply-title-page.test.ts +0 -61
  263. package/tests/titlepage/frontmatter-form.test.ts +0 -115
  264. package/tests/toolbar-actions.test.ts +0 -93
  265. package/tests/voice-commands.test.ts +0 -58
  266. package/tsconfig.json +0 -17
  267. package/vitest.config.ts +0 -11
  268. package/vitest.setup.ts +0 -65
@@ -1,22 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { render, screen } from '@testing-library/react'
3
- import { DocumentProvider } from '../src/context/DocumentContext'
4
- import { LanguageProvider } from '../src/i18n/LanguageContext'
5
- import { TranslationProvider } from '../src/i18n/useTranslation'
6
- import { AppShell } from '../src/shell/AppShell'
7
-
8
- describe('AppShell', () => {
9
- it('renders the AppBar and the ElementToolbar when ribbon is visible', () => {
10
- render(
11
- <TranslationProvider>
12
- <DocumentProvider>
13
- <LanguageProvider>
14
- <AppShell />
15
- </LanguageProvider>
16
- </DocumentProvider>
17
- </TranslationProvider>
18
- )
19
- expect(screen.getByRole('button', { name: /scene navigator/i })).toBeInTheDocument()
20
- expect(screen.getByRole('toolbar', { name: /editing toolbar/i })).toBeInTheDocument()
21
- })
22
- })
@@ -1,36 +0,0 @@
1
- import React from 'react'
2
- import { describe, it, expect } from 'vitest'
3
- import { render, screen } from '@testing-library/react'
4
- import { ElementToolbar } from '../src/shell/ElementToolbar'
5
- import { TranslationProvider } from '../src/i18n/useTranslation'
6
-
7
- describe('ElementToolbar', () => {
8
- it('renders element buttons and B/I/U at the end', () => {
9
- render(
10
- <TranslationProvider>
11
- <ElementToolbar />
12
- </TranslationProvider>
13
- )
14
- expect(screen.getByRole('button', { name: /scene heading/i })).toBeInTheDocument()
15
- expect(screen.getByRole('button', { name: /character/i })).toBeInTheDocument()
16
- const buttons = screen.getAllByRole('button')
17
- const labels = buttons.map(b => b.getAttribute('aria-label'))
18
- expect(labels.indexOf('Bold')).toBeGreaterThan(labels.indexOf('Scene Heading'))
19
- expect(labels).toContain('Underline')
20
- })
21
-
22
- it('renders a button for every screenplay element, including Lyrics', () => {
23
- render(
24
- <TranslationProvider>
25
- <ElementToolbar />
26
- </TranslationProvider>
27
- )
28
- const labels = screen.getAllByRole('button').map(b => b.getAttribute('aria-label'))
29
- for (const name of [
30
- 'Scene Heading', 'Action', 'Character', 'Dialogue', 'Parenthetical',
31
- 'Transition', 'Centered', 'Lyrics', 'Note', 'Page Break', 'Section',
32
- ]) {
33
- expect(labels).toContain(name)
34
- }
35
- })
36
- })
@@ -1,129 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest'
2
- import { render, screen, fireEvent } from '@testing-library/react'
3
- import { LanguageStatus } from '../src/shell/LanguageStatus'
4
- import { LanguageProvider } from '../src/i18n/LanguageContext'
5
- import { DocumentProvider } from '../src/context/DocumentContext'
6
-
7
- function TestWrapper({ children }: { children: React.ReactNode }) {
8
- return (
9
- <DocumentProvider>
10
- <LanguageProvider>{children}</LanguageProvider>
11
- </DocumentProvider>
12
- )
13
- }
14
-
15
- describe('LanguageStatus', () => {
16
- it('renders with current language code', () => {
17
- render(
18
- <TestWrapper>
19
- <LanguageStatus />
20
- </TestWrapper>
21
- )
22
- // Should show "EN" for English (default)
23
- expect(screen.getByText('EN')).toBeInTheDocument()
24
- })
25
-
26
- it('opens menu on button click', () => {
27
- render(
28
- <TestWrapper>
29
- <LanguageStatus />
30
- </TestWrapper>
31
- )
32
- const btn = screen.getByText('EN')
33
- fireEvent.click(btn)
34
- // Menu should be visible with language options
35
- expect(screen.getByText('English')).toBeInTheDocument()
36
- expect(screen.getByText('Tamil')).toBeInTheDocument()
37
- })
38
-
39
- it('closes menu after language selection', () => {
40
- const { container } = render(
41
- <TestWrapper>
42
- <LanguageStatus />
43
- </TestWrapper>
44
- )
45
- const btn = screen.getByText('EN')
46
- fireEvent.click(btn)
47
- const tamilOption = screen.getByText('Tamil')
48
- fireEvent.click(tamilOption)
49
- // Menu should close
50
- expect(container.querySelector('.cs-lang-menu')).not.toBeInTheDocument()
51
- })
52
-
53
- it('highlights current language in menu', () => {
54
- const { container } = render(
55
- <TestWrapper>
56
- <LanguageStatus />
57
- </TestWrapper>
58
- )
59
- const btn = screen.getByText('EN')
60
- fireEvent.click(btn)
61
- // Find the English option in the menu
62
- const menuOptions = container.querySelectorAll('.cs-lang-option')
63
- // First option should be English and should be active
64
- expect(menuOptions[0]).toHaveClass('cs-lang-active')
65
- })
66
-
67
- it('updates document text with language tag on override', () => {
68
- const { container } = render(
69
- <TestWrapper>
70
- <LanguageStatus />
71
- </TestWrapper>
72
- )
73
- const btn = screen.getByText('EN')
74
- fireEvent.click(btn)
75
- const tamilOption = screen.getByText('Tamil')
76
- fireEvent.click(tamilOption)
77
- // Text should have been mutated with language tag
78
- // Menu should be closed
79
- expect(container.querySelector('.cs-lang-menu')).not.toBeInTheDocument()
80
- })
81
-
82
- it('renders all 10 language options in menu', () => {
83
- render(
84
- <TestWrapper>
85
- <LanguageStatus />
86
- </TestWrapper>
87
- )
88
- const btn = screen.getByText('EN')
89
- fireEvent.click(btn)
90
-
91
- const expectedLanguages = [
92
- 'English',
93
- 'Hindi (Devanagari)',
94
- 'Tamil',
95
- 'Telugu',
96
- 'Kannada',
97
- 'Malayalam',
98
- 'Bengali',
99
- 'Gujarati',
100
- 'Gurmukhi',
101
- 'Odia',
102
- ]
103
-
104
- expectedLanguages.forEach(lang => {
105
- expect(screen.getByText(lang)).toBeInTheDocument()
106
- })
107
- })
108
-
109
- it('toggles menu visibility on repeated button clicks', () => {
110
- const { container } = render(
111
- <TestWrapper>
112
- <LanguageStatus />
113
- </TestWrapper>
114
- )
115
- const btn = screen.getByText('EN')
116
-
117
- // Open menu
118
- fireEvent.click(btn)
119
- expect(container.querySelector('.cs-lang-menu')).toBeInTheDocument()
120
-
121
- // Close menu
122
- fireEvent.click(btn)
123
- expect(container.querySelector('.cs-lang-menu')).not.toBeInTheDocument()
124
-
125
- // Open menu again
126
- fireEvent.click(btn)
127
- expect(container.querySelector('.cs-lang-menu')).toBeInTheDocument()
128
- })
129
- })
@@ -1,71 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { render, screen, act, fireEvent } from '@testing-library/react';
3
- import React from 'react';
4
- import { SelectionMenu } from '../src/editor/SelectionMenu';
5
- import { DocumentProvider } from '../src/context/DocumentContext';
6
- import { LanguageProvider } from '../src/i18n/LanguageContext';
7
- import { TranslationProvider } from '../src/i18n/useTranslation';
8
-
9
- describe('SelectionMenu floating toolbar', () => {
10
- beforeEach(() => {
11
- vi.restoreAllMocks();
12
- });
13
-
14
- it('renders nothing when there is no selection', () => {
15
- render(
16
- <TranslationProvider>
17
- <DocumentProvider>
18
- <LanguageProvider>
19
- <SelectionMenu />
20
- </LanguageProvider>
21
- </DocumentProvider>
22
- </TranslationProvider>
23
- );
24
-
25
- expect(screen.queryByText('Format')).toBeNull();
26
- });
27
-
28
- it('renders menu when selection is present inside editor', () => {
29
- const editorDiv = document.createElement('div');
30
- editorDiv.className = 'ProseMirror';
31
-
32
- const mockSelection = {
33
- isCollapsed: false,
34
- rangeCount: 1,
35
- toString: () => 'some unformatted text',
36
- getRangeAt: () => ({
37
- commonAncestorContainer: editorDiv,
38
- getBoundingClientRect: () => ({
39
- left: 100,
40
- width: 200,
41
- top: 150,
42
- height: 20,
43
- }),
44
- }),
45
- };
46
-
47
- vi.spyOn(window, 'getSelection').mockImplementation(() => mockSelection as any);
48
-
49
- render(
50
- <TranslationProvider>
51
- <DocumentProvider>
52
- <LanguageProvider>
53
- <SelectionMenu />
54
- </LanguageProvider>
55
- </DocumentProvider>
56
- </TranslationProvider>
57
- );
58
-
59
- act(() => {
60
- document.dispatchEvent(new Event('selectionchange'));
61
- });
62
-
63
- expect(screen.getByText('Format')).toBeInTheDocument();
64
- expect(screen.getByText('Cut')).toBeInTheDocument();
65
- expect(screen.getByText('Copy')).toBeInTheDocument();
66
-
67
- // Clicking Format should open the confirm dialog
68
- fireEvent.click(screen.getByText('Format'));
69
- expect(screen.getByText('Formatting speech into Sutra screenplay format...')).toBeInTheDocument();
70
- });
71
- });
@@ -1,24 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { render, screen } from '@testing-library/react'
3
- import { ToolbarButton } from '../src/shell/ToolbarButton'
4
-
5
- describe('ToolbarButton', () => {
6
- it('renders label, sigil and shortcut in the tooltip', () => {
7
- render(
8
- <ToolbarButton label="Scene Heading" sigil="##" shortcut="" active={false} onClick={() => {}}>
9
- <span>icon</span>
10
- </ToolbarButton>
11
- )
12
- expect(screen.getByRole('button', { name: /scene heading/i })).toBeInTheDocument()
13
- expect(screen.getByText('##')).toBeInTheDocument()
14
- })
15
-
16
- it('applies active class when active', () => {
17
- render(
18
- <ToolbarButton label="Bold" sigil="" shortcut="Mod-B" active={true} onClick={() => {}}>
19
- <span>B</span>
20
- </ToolbarButton>
21
- )
22
- expect(screen.getByRole('button')).toHaveClass('cs-tb-active')
23
- })
24
- })
@@ -1,102 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { getAIConfig, saveAIConfig, getApiKey, setApiKey, deleteApiKey, fetchProviders, cleanVoiceResponse } from '../src/ai/ai-client';
3
-
4
- describe('AI Client Key Management & Onboarding', () => {
5
- beforeEach(() => {
6
- localStorage.clear();
7
- });
8
-
9
- it('should return default AIConfig when none is saved', () => {
10
- const config = getAIConfig();
11
- expect(config.preferredProvider).toBe('anthropic');
12
- expect(config.preferredModel).toBe('');
13
- expect(config.customProviders).toEqual([]);
14
- });
15
-
16
- it('should save and load AIConfig correctly', () => {
17
- const customConfig = {
18
- preferredProvider: 'groq',
19
- preferredModel: 'llama-3.3-70b-versatile',
20
- customProviders: [
21
- {
22
- id: 'local-ollama',
23
- name: 'Local Ollama',
24
- baseUrl: 'http://localhost:11434/v1',
25
- models: [{ id: 'llama3', name: 'Llama 3' }]
26
- }
27
- ]
28
- };
29
- saveAIConfig(customConfig);
30
- const loaded = getAIConfig();
31
- expect(loaded).toEqual(customConfig);
32
- });
33
-
34
- it('should store and delete API keys in localStorage', async () => {
35
- const geminiKey = 'test-gemini-key-123';
36
- await setApiKey('gemini', geminiKey);
37
- const loadedKey = await getApiKey('gemini');
38
- expect(loadedKey).toBe(geminiKey);
39
-
40
- await deleteApiKey('gemini');
41
- const clearedKey = await getApiKey('gemini');
42
- expect(clearedKey).toBeNull();
43
- });
44
-
45
- it('should merge custom providers with base providers', async () => {
46
- const customConfig = {
47
- preferredProvider: 'local-ollama',
48
- preferredModel: 'llama3',
49
- customProviders: [
50
- {
51
- id: 'local-ollama',
52
- name: 'Local Ollama',
53
- baseUrl: 'http://localhost:11434/v1',
54
- models: [{ id: 'llama3', name: 'Llama 3' }]
55
- }
56
- ]
57
- };
58
- const list = await fetchProviders(customConfig);
59
- const ollama = list.find(p => p.id === 'local-ollama');
60
- expect(ollama).toBeDefined();
61
- expect(ollama?.name).toBe('Local Ollama');
62
- expect(ollama?.models[0]?.id).toBe('llama3');
63
-
64
- // Default provider should still exist
65
- const gemini = list.find(p => p.id === 'gemini');
66
- expect(gemini).toBeDefined();
67
- });
68
-
69
- describe('cleanVoiceResponse', () => {
70
- it('should clean voice responses by stripping code blocks, reasoning, and shorthand comments', () => {
71
- const inputCluttered = `
72
- * Input: Raw voice transcript "New Scene scene #2 Exterior swimming pool daytime."
73
- * Task: Convert to Sutra screenplay format.
74
- * Sutra Rules:
75
- * Scene Headings: \`##\` (e.g., \`## INT. COFFEE SHOP - DAY\`).
76
- * Format: \`## EXT. SWIMMING POOL - DAY {#2}\` (Standard screenplay shorthand for Exterior and Daytime).
77
-
78
- ## EXT. SWIMMING POOL - DAY {#2}
79
- `;
80
- expect(cleanVoiceResponse(inputCluttered)).toBe('## EXT. SWIMMING POOL - DAY {#2}');
81
-
82
- const inputWithCodeBlock = '```cine\n## EXT. SWIMMING POOL - DAY {#2}\n```';
83
- expect(cleanVoiceResponse(inputWithCodeBlock)).toBe('## EXT. SWIMMING POOL - DAY {#2}');
84
-
85
- const inputWithTrailingShorthand = '## EXT. SWIMMING POOL - DAY {#2} (Standard screenplay shorthand for Exterior and Daytime)';
86
- expect(cleanVoiceResponse(inputWithTrailingShorthand)).toBe('## EXT. SWIMMING POOL - DAY {#2}');
87
-
88
- const userGemmaClutter = `
89
- Rules:
90
- Scene ID: Append as \`{#ID}\` at the end.
91
-
92
- "7" -> Scene ID {#7}
93
- "exterior" -> EXT.
94
- "Road" -> ROAD
95
- "evening" -> EVENING
96
-
97
- * \`## EXT. ROAD - EVENING {#7}
98
- `;
99
- expect(cleanVoiceResponse(userGemmaClutter)).toBe('## EXT. ROAD - EVENING {#7}');
100
- });
101
- });
102
- });
@@ -1,72 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest'
2
- import { render, screen, act } from '@testing-library/react'
3
- import React from 'react'
4
- import { formatActiveCalls, subscribeToAICalls, mockActiveCallsList } from '../src/ai/ai-client'
5
- import { AppShell } from '../src/shell/AppShell'
6
- import { DocumentProvider } from '../src/context/DocumentContext'
7
- import { LanguageProvider } from '../src/i18n/LanguageContext'
8
- import { TranslationProvider } from '../src/i18n/useTranslation'
9
-
10
- describe('AI Status Bar indicator', () => {
11
- it('formats active calls correctly', () => {
12
- expect(formatActiveCalls([])).toBe('')
13
- expect(formatActiveCalls([{ providerName: 'Gemini', model: 'gemma-31b' }])).toBe('Calling Gemini (gemma-31b)...')
14
- expect(formatActiveCalls([
15
- { providerName: 'Gemini', model: 'gemma-31b' },
16
- { providerName: 'Gemini', model: 'gemma-31b' },
17
- ])).toBe('Calling Gemini (gemma-31b) (2 calls)...')
18
- expect(formatActiveCalls([
19
- { providerName: 'Gemini', model: 'gemma-31b' },
20
- { providerName: 'Groq', model: 'llama3' },
21
- ])).toBe('Calling Gemini (gemma-31b), Groq (llama3)...')
22
- })
23
-
24
- it('subscribes and notifies when calls change', () => {
25
- const cb = vi.fn()
26
- const unsubscribe = subscribeToAICalls(cb)
27
- expect(cb).toHaveBeenCalledWith([])
28
-
29
- act(() => {
30
- mockActiveCallsList([{ providerName: 'Groq', model: 'llama3' }])
31
- })
32
- expect(cb).toHaveBeenLastCalledWith([{ providerName: 'Groq', model: 'llama3' }])
33
-
34
- act(() => {
35
- mockActiveCallsList([])
36
- })
37
- expect(cb).toHaveBeenLastCalledWith([])
38
-
39
- unsubscribe()
40
- })
41
-
42
- it('renders status indicator inside AppShell', () => {
43
- render(
44
- <TranslationProvider>
45
- <DocumentProvider>
46
- <LanguageProvider>
47
- <AppShell />
48
- </LanguageProvider>
49
- </DocumentProvider>
50
- </TranslationProvider>
51
- )
52
-
53
- // Initially, no indicator is rendered (since calls list is empty)
54
- expect(screen.queryByText(/Calling/)).toBeNull()
55
-
56
- // Mock active calls
57
- act(() => {
58
- mockActiveCallsList([{ providerName: 'Google AI Studio', model: 'gemma-31b' }])
59
- })
60
-
61
- // Now it should be displayed
62
- expect(screen.getByText('Calling Google AI Studio (gemma-31b)...')).toBeInTheDocument()
63
-
64
- // Clear calls
65
- act(() => {
66
- mockActiveCallsList([])
67
- })
68
-
69
- // Indicator should be gone
70
- expect(screen.queryByText(/Calling/)).toBeNull()
71
- })
72
- })
@@ -1,94 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest';
2
- import { extractJson, listModels, hasConfiguredApiKey, callAI, setApiKey, deleteApiKey, ProviderConfig } from '../src/ai/ai-client';
3
-
4
- describe('extractJson', () => {
5
- it('parses a clean JSON object', () => {
6
- expect(extractJson<{ synopsis: string; duration: string }>('{"synopsis":"A meets B.","duration":"01:30"}'))
7
- .toEqual({ synopsis: 'A meets B.', duration: '01:30' });
8
- });
9
-
10
- it('parses JSON wrapped in a markdown fence', () => {
11
- const raw = '```json\n{"synopsis":"A meets B.","duration":"01:30"}\n```';
12
- expect(extractJson<{ synopsis: string; duration: string }>(raw))
13
- .toEqual({ synopsis: 'A meets B.', duration: '01:30' });
14
- });
15
-
16
- it('recovers a JSON object embedded in stray commentary', () => {
17
- const raw = 'Sure, here is the result:\n{"synopsis":"A meets B.","duration":"01:30"}\nHope that helps!';
18
- expect(extractJson<{ synopsis: string; duration: string }>(raw))
19
- .toEqual({ synopsis: 'A meets B.', duration: '01:30' });
20
- });
21
-
22
- it('throws with the raw text attached when nothing parses', () => {
23
- expect(() => extractJson('not json at all')).toThrow(/AI did not return valid JSON/);
24
- });
25
- });
26
-
27
- describe('listModels', () => {
28
- it('falls back to the static model list when the live endpoint is unreachable', async () => {
29
- const provider: ProviderConfig = {
30
- id: 'made-up-provider',
31
- name: 'Made Up Provider',
32
- baseUrl: 'not-a-real-url',
33
- models: [{ id: 'static-model', name: 'Static Model' }],
34
- };
35
- const models = await listModels(provider, 'fake-key');
36
- expect(models).toEqual(provider.models);
37
- });
38
- });
39
-
40
- describe('preferred-model resolution (regression: wrong provider/model used)', () => {
41
- beforeEach(() => {
42
- localStorage.clear();
43
- });
44
-
45
- it('sends the user-selected model for the preferred provider, even when that model is not in the static provider list', async () => {
46
- // Reproduces the reported bug: the user picks a model from a
47
- // provider's *live* /models listing (e.g. "qwen/qwen3.8-27b" for
48
- // Groq) via Settings, which is never a member of the narrow static
49
- // generated-providers.json list. That used to get silently discarded
50
- // and replaced with the static list's first (often stale/retired)
51
- // model, making the call fail — or, if another provider's key also
52
- // happened to be configured, silently fall through to a call against
53
- // a completely different provider.
54
- const originalFetch = global.fetch;
55
- const sentBodies: any[] = [];
56
- global.fetch = vi.fn(async (url: any, init: any) => {
57
- if (String(url).includes('api.groq.com')) {
58
- sentBodies.push(JSON.parse(init.body));
59
- return new Response(JSON.stringify({ choices: [{ message: { content: 'ok' } }] }), { status: 200 });
60
- }
61
- throw new Error('network unavailable in test');
62
- }) as any;
63
-
64
- try {
65
- await setApiKey('groq', 'test-groq-key');
66
- const config = { preferredProvider: 'groq', preferredModel: 'qwen/qwen3.8-27b', customProviders: [] };
67
-
68
- const result = await callAI('hello', 'system prompt', config);
69
-
70
- expect(result).toBe('ok');
71
- expect(sentBodies).toHaveLength(1);
72
- expect(sentBodies[0].model).toBe('qwen/qwen3.8-27b');
73
- } finally {
74
- global.fetch = originalFetch;
75
- await deleteApiKey('groq');
76
- }
77
- });
78
- });
79
-
80
- describe('hasConfiguredApiKey', () => {
81
- beforeEach(() => {
82
- localStorage.clear();
83
- });
84
-
85
- it('is false with no stored keys and true once one provider has a key', async () => {
86
- const config = { preferredProvider: 'anthropic', preferredModel: '', customProviders: [] };
87
- expect(await hasConfiguredApiKey(config)).toBe(false);
88
-
89
- await setApiKey('anthropic', 'test-key');
90
- expect(await hasConfiguredApiKey(config)).toBe(true);
91
-
92
- await deleteApiKey('anthropic');
93
- });
94
- });
@@ -1,20 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { createAutocompletePlugin } from '../../src/autocomplete/AutocompletePlugin'
3
-
4
- describe('AutocompletePlugin', () => {
5
- it('createAutocompletePlugin returns a ProseMirror Plugin', () => {
6
- const plugin = createAutocompletePlugin()
7
- expect(plugin).toBeDefined()
8
- expect(typeof plugin.spec).toBe('object')
9
- })
10
-
11
- it('plugin has handleKeyDown prop', () => {
12
- const plugin = createAutocompletePlugin()
13
- expect(typeof plugin.spec.props?.handleKeyDown).toBe('function')
14
- })
15
-
16
- it('plugin has view prop', () => {
17
- const plugin = createAutocompletePlugin()
18
- expect(typeof plugin.spec.view).toBe('function')
19
- })
20
- })
@@ -1,106 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import {
3
- buildCharacterCandidates,
4
- buildLocationCandidates,
5
- buildTransitionCandidates,
6
- buildMetadataKeyCandidates,
7
- filterCandidates,
8
- } from '../../src/autocomplete/suggestions'
9
-
10
- const SAMPLE = `## INT. LIVING ROOM - DAY
11
- & location: INT. LIVING ROOM
12
-
13
- @RIYA
14
- Hello.
15
-
16
- @ARJUN (V.O.)
17
- I remember that night.
18
-
19
- ## EXT. STREET - NIGHT
20
-
21
- >> FADE TO:
22
-
23
- & status: done
24
- & mood: tense
25
- `
26
-
27
- describe('suggestions', () => {
28
- describe('buildCharacterCandidates', () => {
29
- it('returns character names from @ cues', () => {
30
- const cands = buildCharacterCandidates(SAMPLE)
31
- const labels = cands.map(c => c.label)
32
- expect(labels).toContain('RIYA')
33
- expect(labels).toContain('ARJUN (V.O.)')
34
- })
35
-
36
- it('all have type "character"', () => {
37
- const cands = buildCharacterCandidates(SAMPLE)
38
- expect(cands.every(c => c.type === 'character')).toBe(true)
39
- })
40
- })
41
-
42
- describe('buildLocationCandidates', () => {
43
- it('extracts locations from scene headings', () => {
44
- const cands = buildLocationCandidates(SAMPLE)
45
- const labels = cands.map(c => c.label)
46
- expect(labels).toContain('INT. LIVING ROOM')
47
- expect(labels).toContain('EXT. STREET')
48
- })
49
-
50
- it('all have type "location"', () => {
51
- const cands = buildLocationCandidates(SAMPLE)
52
- expect(cands.every(c => c.type === 'location')).toBe(true)
53
- })
54
- })
55
-
56
- describe('buildTransitionCandidates', () => {
57
- it('includes predefined transitions', () => {
58
- const cands = buildTransitionCandidates(SAMPLE)
59
- const labels = cands.map(c => c.label)
60
- expect(labels).toContain('CUT TO:')
61
- expect(labels).toContain('FADE IN:')
62
- })
63
-
64
- it('includes transitions used in text', () => {
65
- const cands = buildTransitionCandidates(SAMPLE)
66
- const labels = cands.map(c => c.label)
67
- expect(labels).toContain('FADE TO:')
68
- })
69
- })
70
-
71
- describe('buildMetadataKeyCandidates', () => {
72
- it('includes reserved keys', () => {
73
- const cands = buildMetadataKeyCandidates(SAMPLE)
74
- const labels = cands.map(c => c.label)
75
- expect(labels).toContain('status')
76
- expect(labels).toContain('location')
77
- })
78
-
79
- it('includes keys used in text', () => {
80
- const cands = buildMetadataKeyCandidates(SAMPLE)
81
- const labels = cands.map(c => c.label)
82
- expect(labels).toContain('mood')
83
- })
84
- })
85
-
86
- describe('filterCandidates', () => {
87
- it('filters by prefix case-insensitively', () => {
88
- const cands = [
89
- { label: 'RIYA', type: 'character' as const },
90
- { label: 'ARJUN', type: 'character' as const },
91
- { label: 'RAJ', type: 'character' as const },
92
- ]
93
- const result = filterCandidates(cands, 'ri')
94
- expect(result.length).toBe(1)
95
- expect(result[0]?.label).toBe('RIYA')
96
- })
97
-
98
- it('returns all candidates for empty prefix', () => {
99
- const cands = [
100
- { label: 'A', type: 'character' as const },
101
- { label: 'B', type: 'character' as const },
102
- ]
103
- expect(filterCandidates(cands, '').length).toBe(2)
104
- })
105
- })
106
- })