Skip to content

Commit ddd3689

Browse files
committed
#171 add tests
Prompt: Add tests for the current editor flow in SourceEditor, SourceEditorCard, and helpers. Cover initialization, read-only toggling, replace-content behavior, metadata/header handling, and the pane button visibility logic. Remove or replace any skipped tests that are now stale. Keep the tests focused on the current implementation, not the old textarea-based flow. After editing, run the test suite and fix any failing assertions. Co-authored-by: GPT-5.4 mini <gpt-5.4-mini@openai.com>
1 parent d8fb855 commit ddd3689

4 files changed

Lines changed: 388 additions & 95 deletions

File tree

test/helpers.test.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
jest.mock('../src/components/sourceEditor/SourceEditorCard', () => {
2+
return class SourceEditorCard {}
3+
})
4+
5+
const { getResponseHeaders, fetchContentAndMetadata, setUnedited } = require('../src/helpers')
6+
7+
describe('helpers', () => {
8+
beforeEach(() => {
9+
document.body.innerHTML = ''
10+
jest.clearAllMocks()
11+
})
12+
13+
it('reads response headers into metadata', () => {
14+
const response = {
15+
headers: {
16+
get: jest.fn((name) => {
17+
if (name === 'content-type') return 'text/turtle; charset=utf-8'
18+
if (name === 'allow') return 'GET,PUT'
19+
if (name === 'etag') return '"abc"'
20+
return null
21+
}),
22+
},
23+
}
24+
const store = { each: jest.fn(), any: jest.fn(), anyValue: jest.fn(), sym: jest.fn() }
25+
const subject = { uri: 'https://example.org/profile/card' }
26+
27+
expect(getResponseHeaders(store, subject, response)).toEqual({
28+
contentType: 'text/turtle',
29+
allowed: 'GET,PUT',
30+
eTag: '"abc"',
31+
})
32+
})
33+
34+
it('fetches content and applies the returned metadata', async () => {
35+
const response = {
36+
ok: true,
37+
headers: {
38+
get: jest.fn((name) => {
39+
if (name === 'content-type') return 'text/turtle'
40+
if (name === 'allow') return 'GET,PUT'
41+
if (name === 'etag') return '"abc"'
42+
return null
43+
}),
44+
},
45+
responseText: '<> a <#Thing>.',
46+
}
47+
const store = {
48+
fetcher: {
49+
webOperation: jest.fn().mockResolvedValue(response),
50+
},
51+
each: jest.fn(),
52+
any: jest.fn(),
53+
anyValue: jest.fn(),
54+
sym: jest.fn(),
55+
}
56+
const subject = { uri: 'https://example.org/profile/card' }
57+
const sourcePaneState = { broken: false, contentType: undefined, allowed: undefined, eTag: undefined }
58+
59+
const result = await fetchContentAndMetadata(store, subject, sourcePaneState)
60+
61+
expect(result).toEqual({
62+
content: '<> a <#Thing>.',
63+
metadata: {
64+
contentType: 'text/turtle',
65+
allowed: 'GET,PUT',
66+
eTag: '"abc"',
67+
},
68+
})
69+
expect(sourcePaneState).toEqual({
70+
broken: false,
71+
contentType: 'text/turtle',
72+
allowed: 'GET,PUT',
73+
eTag: '"abc"',
74+
})
75+
})
76+
77+
it('sets the editor back to read only and shows the edit controls', () => {
78+
const editorCard = document.createElement('source-editor-card')
79+
editorCard.setReadOnly = jest.fn()
80+
document.body.appendChild(editorCard)
81+
82+
const saveButton = document.createElement('button')
83+
saveButton.className = 'sourcePaneSaveButton'
84+
const editButton = document.createElement('button')
85+
editButton.className = 'sourcePaneEditButton'
86+
const compactButton = document.createElement('button')
87+
compactButton.className = 'sourcePaneCompactButton'
88+
document.body.appendChild(saveButton)
89+
document.body.appendChild(editButton)
90+
document.body.appendChild(compactButton)
91+
92+
setUnedited(
93+
{ uri: 'https://example.org/profile/card' },
94+
{ broken: false, contentType: 'text/turtle', allowed: 'GET', eTag: '"abc"' }
95+
)
96+
97+
expect(editorCard.setReadOnly).toHaveBeenCalledWith(true)
98+
expect(saveButton.className).toContain('sourcePaneControlHidden')
99+
expect(editButton.className).toContain('sourcePaneControlVisible')
100+
expect(compactButton.className).toContain('sourcePaneControlVisible')
101+
})
102+
})

test/sourceEditor.test.js

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
jest.mock('@codemirror/state', () => {
2+
class Compartment {
3+
of(value) {
4+
return { type: 'compartment-of', value }
5+
}
6+
7+
reconfigure(value) {
8+
return { type: 'reconfigure', value }
9+
}
10+
}
11+
12+
const EditorState = {
13+
create: jest.fn((config) => ({ ...config }))
14+
}
15+
16+
return { Compartment, EditorState }
17+
})
18+
19+
const createdViews = []
20+
21+
jest.mock('@codemirror/view', () => {
22+
class EditorView {
23+
constructor({ state, parent }) {
24+
this.state = state
25+
this.parent = parent
26+
this.dom = globalThis.document.createElement('div')
27+
this.dispatch = jest.fn((transaction) => {
28+
this.lastTransaction = transaction
29+
if (transaction.changes) {
30+
this.state.doc = transaction.changes.insert
31+
}
32+
})
33+
this.focus = jest.fn()
34+
this.destroy = jest.fn()
35+
createdViews.push(this)
36+
if (parent) {
37+
parent.appendChild(this.dom)
38+
}
39+
}
40+
}
41+
42+
EditorView.editable = {
43+
of: jest.fn((value) => ({ type: 'editable', value }))
44+
}
45+
EditorView.theme = jest.fn((spec, options) => ({ type: 'theme', spec, options }))
46+
EditorView.lineWrapping = { type: 'lineWrapping' }
47+
EditorView.updateListener = {
48+
of: jest.fn((listener) => ({ type: 'updateListener', listener }))
49+
}
50+
51+
return {
52+
EditorView,
53+
drawSelection: jest.fn(() => ({ type: 'drawSelection' })),
54+
keymap: {
55+
of: jest.fn((value) => ({ type: 'keymap', value }))
56+
},
57+
lineNumbers: jest.fn(() => ({ type: 'lineNumbers' }))
58+
}
59+
})
60+
61+
jest.mock('@codemirror/language', () => ({
62+
defaultHighlightStyle: { name: 'defaultHighlightStyle' },
63+
syntaxHighlighting: jest.fn((style, options) => ({ type: 'syntaxHighlighting', style, options })),
64+
HighlightStyle: {
65+
define: jest.fn(() => ({ type: 'highlightStyle' }))
66+
},
67+
StreamLanguage: class {
68+
static define(mode) {
69+
return { type: 'stream-language', mode }
70+
}
71+
}
72+
}))
73+
74+
jest.mock('@codemirror/commands', () => ({
75+
defaultKeymap: [{ key: 'default' }],
76+
history: jest.fn(() => ({ type: 'history' })),
77+
historyKeymap: [{ key: 'history' }]
78+
}))
79+
80+
jest.mock('@codemirror/theme-one-dark', () => ({
81+
oneDark: { name: 'oneDark' }
82+
}))
83+
84+
const { SourceEditor } = require('../src/components/sourceEditor/SourceEditor')
85+
86+
describe('SourceEditor', () => {
87+
beforeEach(() => {
88+
createdViews.length = 0
89+
document.body.innerHTML = ''
90+
jest.clearAllMocks()
91+
})
92+
93+
it('initializes with the provided document text', async () => {
94+
const editor = new SourceEditor()
95+
const container = document.createElement('div')
96+
document.body.appendChild(container)
97+
98+
await editor.initialize(container, 'hello world', 'text/plain')
99+
100+
expect(editor.getValue()).toBe('hello world')
101+
expect(createdViews).toHaveLength(1)
102+
expect(createdViews[0].state.doc).toBe('hello world')
103+
expect(createdViews[0].parent).toBe(container)
104+
})
105+
106+
it('replaces content and toggles read only state', async () => {
107+
const editor = new SourceEditor()
108+
const container = document.createElement('div')
109+
document.body.appendChild(container)
110+
111+
await editor.initialize(container, 'first', 'text/plain')
112+
const view = createdViews[0]
113+
114+
editor.replaceContent('second')
115+
expect(editor.getValue()).toBe('second')
116+
expect(view.dispatch).toHaveBeenCalledWith({
117+
changes: { from: 0, to: 5, insert: 'second' }
118+
})
119+
120+
editor.setReadOnly(true)
121+
expect(view.dispatch).toHaveBeenLastCalledWith({
122+
effects: { type: 'reconfigure', value: { type: 'editable', value: false } }
123+
})
124+
125+
editor.focusEditor()
126+
expect(view.focus).toHaveBeenCalled()
127+
})
128+
})

test/sourceEditorCard.test.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
jest.mock('../src/helpers', () => ({
2+
fetchContentAndMetadata: jest.fn(),
3+
setUnedited: jest.fn(),
4+
}))
5+
6+
jest.mock('../src/components/sourceEditor/SourceEditor', () => {
7+
return {
8+
SourceEditor: jest.fn().mockImplementation(() => ({
9+
initialize: jest.fn(),
10+
getValue: jest.fn(() => 'editor value'),
11+
focusEditor: jest.fn(),
12+
setReadOnly: jest.fn(),
13+
replaceContent: jest.fn(),
14+
})),
15+
}
16+
})
17+
18+
const { fetchContentAndMetadata, setUnedited } = require('../src/helpers')
19+
const { SourceEditor } = require('../src/components/sourceEditor/SourceEditor')
20+
require('../src/components/sourceEditor/SourceEditorCard')
21+
22+
describe('source-editor-card', () => {
23+
beforeEach(() => {
24+
document.body.innerHTML = ''
25+
jest.clearAllMocks()
26+
})
27+
28+
it('renders the filename and initializes the editor with fetched content', async () => {
29+
fetchContentAndMetadata.mockResolvedValue({
30+
content: 'hello world',
31+
metadata: {
32+
contentType: 'text/turtle',
33+
allowed: 'GET',
34+
eTag: '"123"',
35+
},
36+
})
37+
38+
const card = document.createElement('source-editor-card')
39+
card.store = { fetcher: {} }
40+
card.subject = {
41+
uri: 'https://testingsolidos.solidcommunity.net/profile/card',
42+
value: 'https://testingsolidos.solidcommunity.net/profile/card',
43+
}
44+
card.sourcePaneState = {
45+
broken: false,
46+
contentType: undefined,
47+
allowed: undefined,
48+
eTag: undefined,
49+
}
50+
51+
document.body.appendChild(card)
52+
await card.updateComplete
53+
54+
await new Promise((resolve) => setTimeout(resolve, 0))
55+
56+
const editorInstance = SourceEditor.mock.results[0].value
57+
expect(fetchContentAndMetadata).toHaveBeenCalledWith(card.store, card.subject, card.sourcePaneState)
58+
expect(editorInstance.initialize).toHaveBeenCalled()
59+
expect(setUnedited).toHaveBeenCalledWith(card.subject, card.sourcePaneState)
60+
expect(card.shadowRoot.textContent).toContain('card')
61+
})
62+
63+
it('delegates editor API methods', () => {
64+
const card = document.createElement('source-editor-card')
65+
const editorInstance = SourceEditor.mock.results[0]?.value ?? {
66+
getValue: jest.fn(() => 'editor value'),
67+
focusEditor: jest.fn(),
68+
setReadOnly: jest.fn(),
69+
replaceContent: jest.fn(),
70+
}
71+
card._editor = editorInstance
72+
73+
expect(card.getValue()).toBe('editor value')
74+
75+
card.focusEditor()
76+
expect(editorInstance.focusEditor).toHaveBeenCalled()
77+
78+
card.setReadOnly(true)
79+
expect(editorInstance.setReadOnly).toHaveBeenCalledWith(true)
80+
81+
card.setValue('updated')
82+
expect(editorInstance.replaceContent).toHaveBeenCalledWith('updated')
83+
})
84+
})

0 commit comments

Comments
 (0)