enigmatic 0.27.0 → 0.29.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.
- package/CLIENT_JS_DOCS.md +322 -0
- package/__tests__/e2.test.js +270 -52
- package/__tests__/jest.config.js +7 -0
- package/{jest.setup.js → __tests__/jest.setup.js} +0 -1
- package/bun-server.js +130 -0
- package/package.json +12 -12
- package/public/client.js +98 -0
- package/public/custom.js +29 -0
- package/public/index.html +45 -0
- package/README.md +0 -218
- package/__tests__/enigmatic.test.js +0 -328
- package/components.js +0 -58
- package/e2.js +0 -38
- package/enigmatic.js +0 -248
- package/index.html +0 -34
- package/jest.config.js +0 -7
- /package/{enigmatic.css → public/client.css} +0 -0
- /package/{theme.css → public/theme.css} +0 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# client.js Documentation
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`client.js` is a client-side JavaScript library that provides utilities for DOM manipulation, reactive state management, and API interactions with a backend server. It automatically initializes custom HTML elements and provides a simple API for key-value storage, file operations, and authentication.
|
|
6
|
+
|
|
7
|
+
## Core Utilities
|
|
8
|
+
|
|
9
|
+
### DOM Selectors
|
|
10
|
+
|
|
11
|
+
```javascript
|
|
12
|
+
window.$ // Alias for document.querySelector
|
|
13
|
+
window.$$ // Alias for document.querySelectorAll
|
|
14
|
+
window.$c // Alias for element.closest (requires $0 context)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**Usage:**
|
|
18
|
+
```javascript
|
|
19
|
+
const element = window.$('#my-id');
|
|
20
|
+
const elements = window.$$('.my-class');
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### API Base URL
|
|
24
|
+
|
|
25
|
+
```javascript
|
|
26
|
+
window.api_url = "https://localhost:3000"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Configures the base URL for all API requests. Modify this to point to your server.
|
|
30
|
+
|
|
31
|
+
## Reactive State Management
|
|
32
|
+
|
|
33
|
+
`window.state` is a Proxy object that automatically updates DOM elements when properties change.
|
|
34
|
+
|
|
35
|
+
**How it works:**
|
|
36
|
+
- Set a property: `window.state.myKey = 'value'`
|
|
37
|
+
- Elements with `data="myKey"` attribute are automatically updated
|
|
38
|
+
- The system looks for custom element handlers in `window.custom[tagName]`
|
|
39
|
+
- Supports both function and object-based custom elements
|
|
40
|
+
|
|
41
|
+
**Example:**
|
|
42
|
+
```html
|
|
43
|
+
<div data="message">Initial</div>
|
|
44
|
+
<script>
|
|
45
|
+
window.state.message = "Updated!"; // Automatically updates the div
|
|
46
|
+
</script>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Custom Element Integration:**
|
|
50
|
+
- If `window.custom[tagName]` is a function: calls `f(value)` and sets `innerHTML`
|
|
51
|
+
- If `window.custom[tagName]` is an object: calls `f.render(value)` and sets `innerHTML`
|
|
52
|
+
|
|
53
|
+
## API Functions
|
|
54
|
+
|
|
55
|
+
All API functions are async and return Promises. They use `window.api_url` as the base URL.
|
|
56
|
+
|
|
57
|
+
### KV Storage Operations
|
|
58
|
+
|
|
59
|
+
#### `window.get(key)`
|
|
60
|
+
Retrieves a value from the server's key-value store.
|
|
61
|
+
|
|
62
|
+
```javascript
|
|
63
|
+
const value = await window.get('my-key');
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**HTTP Method:** GET
|
|
67
|
+
**Endpoint:** `{api_url}/{key}`
|
|
68
|
+
**Returns:** Parsed JSON response
|
|
69
|
+
|
|
70
|
+
#### `window.set(key, value)`
|
|
71
|
+
Stores a value in the server's key-value store.
|
|
72
|
+
|
|
73
|
+
```javascript
|
|
74
|
+
await window.set('my-key', 'my-value');
|
|
75
|
+
await window.set('my-key', { json: 'object' });
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**HTTP Method:** POST
|
|
79
|
+
**Endpoint:** `{api_url}/{key}`
|
|
80
|
+
**Body:** String values sent as-is, objects are JSON stringified
|
|
81
|
+
**Returns:** Parsed JSON response
|
|
82
|
+
|
|
83
|
+
#### `window.delete(key)`
|
|
84
|
+
Deletes a key from the server's key-value store.
|
|
85
|
+
|
|
86
|
+
```javascript
|
|
87
|
+
await window.delete('my-key');
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**HTTP Method:** DELETE
|
|
91
|
+
**Endpoint:** `{api_url}/{key}`
|
|
92
|
+
**Returns:** Parsed JSON response
|
|
93
|
+
|
|
94
|
+
### R2 Storage Operations (File Storage)
|
|
95
|
+
|
|
96
|
+
#### `window.put(key, body)`
|
|
97
|
+
Uploads a file or data to R2 storage.
|
|
98
|
+
|
|
99
|
+
```javascript
|
|
100
|
+
await window.put('filename.txt', 'file content');
|
|
101
|
+
await window.put('image.png', blob);
|
|
102
|
+
await window.put('data.json', { json: 'data' });
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**HTTP Method:** PUT
|
|
106
|
+
**Endpoint:** `{api_url}/{key}`
|
|
107
|
+
**Body:** Accepts Blob, string, or JSON-serializable objects
|
|
108
|
+
**Returns:** Parsed JSON response
|
|
109
|
+
|
|
110
|
+
#### `window.purge(key)`
|
|
111
|
+
Deletes a file from R2 storage.
|
|
112
|
+
|
|
113
|
+
```javascript
|
|
114
|
+
await window.purge('filename.txt');
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**HTTP Method:** PURGE
|
|
118
|
+
**Endpoint:** `{api_url}/{key}`
|
|
119
|
+
**Returns:** Parsed JSON response
|
|
120
|
+
|
|
121
|
+
#### `window.list()`
|
|
122
|
+
Lists all files in the current user's R2 storage.
|
|
123
|
+
|
|
124
|
+
```javascript
|
|
125
|
+
const files = await window.list();
|
|
126
|
+
// Returns: [{ name: 'file1.txt', lastModified: '...', size: 123 }, ...]
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**HTTP Method:** PROPFIND
|
|
130
|
+
**Endpoint:** `{api_url}/` (base URL, no key)
|
|
131
|
+
**Returns:** Array of file objects with `name`, `lastModified`, and `size` properties
|
|
132
|
+
|
|
133
|
+
#### `window.download(key)`
|
|
134
|
+
Downloads a file from R2 storage and triggers browser download.
|
|
135
|
+
|
|
136
|
+
```javascript
|
|
137
|
+
await window.download('filename.txt');
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**HTTP Method:** PATCH
|
|
141
|
+
**Endpoint:** `{api_url}/{key}`
|
|
142
|
+
**Behavior:**
|
|
143
|
+
- Fetches file as blob
|
|
144
|
+
- Creates temporary download URL
|
|
145
|
+
- Triggers browser download
|
|
146
|
+
- Cleans up temporary URL
|
|
147
|
+
|
|
148
|
+
**Note:** Uses PATCH method due to browser limitations with custom HTTP methods.
|
|
149
|
+
|
|
150
|
+
### Authentication
|
|
151
|
+
|
|
152
|
+
#### `window.login()`
|
|
153
|
+
Redirects to the server's login endpoint.
|
|
154
|
+
|
|
155
|
+
```javascript
|
|
156
|
+
window.login();
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Behavior:** Sets `window.location.href` to `{api_url}/login`
|
|
160
|
+
|
|
161
|
+
#### `window.logout()`
|
|
162
|
+
Redirects to the server's logout endpoint.
|
|
163
|
+
|
|
164
|
+
```javascript
|
|
165
|
+
window.logout();
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**Behavior:** Sets `window.location.href` to `{api_url}/logout`
|
|
169
|
+
|
|
170
|
+
## Custom Elements System
|
|
171
|
+
|
|
172
|
+
Custom elements are defined in `window.custom` object and automatically initialized when the DOM loads.
|
|
173
|
+
|
|
174
|
+
### Initialization
|
|
175
|
+
|
|
176
|
+
The library automatically:
|
|
177
|
+
1. Waits for DOM to be ready (`DOMContentLoaded` or immediate if already loaded)
|
|
178
|
+
2. Iterates through all keys in `window.custom`
|
|
179
|
+
3. Finds all matching HTML elements by tag name
|
|
180
|
+
4. Calls the custom element handler and sets `innerHTML`
|
|
181
|
+
|
|
182
|
+
### Defining Custom Elements
|
|
183
|
+
|
|
184
|
+
#### Function-based Custom Element
|
|
185
|
+
|
|
186
|
+
```javascript
|
|
187
|
+
window.custom = {
|
|
188
|
+
"my-element": async (data) => {
|
|
189
|
+
return `<div>Content: ${data}</div>`;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**HTML Usage:**
|
|
195
|
+
```html
|
|
196
|
+
<my-element></my-element>
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
When `window.state.myKey = 'value'` is set and an element has `data="myKey"`:
|
|
200
|
+
```html
|
|
201
|
+
<my-element data="myKey"></my-element>
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The function receives the state value as the first parameter.
|
|
205
|
+
|
|
206
|
+
#### Object-based Custom Element
|
|
207
|
+
|
|
208
|
+
```javascript
|
|
209
|
+
window.custom = {
|
|
210
|
+
"my-element": {
|
|
211
|
+
prop: (data) => `Processed: ${data}`,
|
|
212
|
+
render: function(data) {
|
|
213
|
+
return `<div>${this.prop(data)}</div>`;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**HTML Usage:**
|
|
220
|
+
```html
|
|
221
|
+
<my-element></my-element>
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
When used with reactive state, the `render` method is called with the state value.
|
|
225
|
+
|
|
226
|
+
### Example: File Widget
|
|
227
|
+
|
|
228
|
+
```javascript
|
|
229
|
+
window.custom = {
|
|
230
|
+
"file-widget": async () => {
|
|
231
|
+
const list = await window.list();
|
|
232
|
+
// Returns HTML string with file list and upload button
|
|
233
|
+
return `<div>...</div>`;
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**HTML Usage:**
|
|
239
|
+
```html
|
|
240
|
+
<file-widget></file-widget>
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
This custom element:
|
|
244
|
+
- Fetches file list using `window.list()`
|
|
245
|
+
- Renders file items with download and delete buttons
|
|
246
|
+
- Includes an upload button
|
|
247
|
+
- Uses inline event handlers that call `window.download()`, `window.purge()`, and `window.put()`
|
|
248
|
+
|
|
249
|
+
## Error Handling
|
|
250
|
+
|
|
251
|
+
All API functions throw errors if the request fails. Use try-catch or `.catch()`:
|
|
252
|
+
|
|
253
|
+
```javascript
|
|
254
|
+
try {
|
|
255
|
+
await window.get('nonexistent');
|
|
256
|
+
} catch (err) {
|
|
257
|
+
console.error('Error:', err);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Or with promises
|
|
261
|
+
window.get('key').catch(err => console.error(err));
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Complete Example
|
|
265
|
+
|
|
266
|
+
```html
|
|
267
|
+
<!DOCTYPE html>
|
|
268
|
+
<html>
|
|
269
|
+
<head>
|
|
270
|
+
<script src="custom.js"></script>
|
|
271
|
+
<script src="client.js"></script>
|
|
272
|
+
</head>
|
|
273
|
+
<body>
|
|
274
|
+
<!-- Custom element -->
|
|
275
|
+
<file-widget></file-widget>
|
|
276
|
+
|
|
277
|
+
<!-- Reactive state element -->
|
|
278
|
+
<div data="message">Initial</div>
|
|
279
|
+
|
|
280
|
+
<script>
|
|
281
|
+
// Set reactive state
|
|
282
|
+
window.state.message = "Hello World";
|
|
283
|
+
|
|
284
|
+
// Use API functions
|
|
285
|
+
(async () => {
|
|
286
|
+
await window.set('test', 'value');
|
|
287
|
+
const value = await window.get('test');
|
|
288
|
+
console.log(value);
|
|
289
|
+
|
|
290
|
+
// Upload file
|
|
291
|
+
const fileInput = document.querySelector('input[type="file"]');
|
|
292
|
+
fileInput.onchange = async (e) => {
|
|
293
|
+
const file = e.target.files[0];
|
|
294
|
+
await window.put(file.name, file);
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// List files
|
|
298
|
+
const files = await window.list();
|
|
299
|
+
console.log(files);
|
|
300
|
+
})();
|
|
301
|
+
</script>
|
|
302
|
+
</body>
|
|
303
|
+
</html>
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## Dependencies
|
|
307
|
+
|
|
308
|
+
- Requires `custom.js` to be loaded before `client.js` if using custom elements
|
|
309
|
+
- Requires a backend server that implements the API endpoints
|
|
310
|
+
- Requires browser support for:
|
|
311
|
+
- `fetch` API
|
|
312
|
+
- `Proxy` API
|
|
313
|
+
- `Blob` API
|
|
314
|
+
- `URL.createObjectURL`
|
|
315
|
+
|
|
316
|
+
## Notes
|
|
317
|
+
|
|
318
|
+
- All API functions automatically encode keys using `encodeURIComponent`
|
|
319
|
+
- The `window.download()` function uses PATCH method internally (browsers don't support custom HTTP methods)
|
|
320
|
+
- Custom elements are initialized once on page load; use `location.reload()` to refresh
|
|
321
|
+
- The reactive state system only updates elements with matching `data` attributes
|
|
322
|
+
- Custom element handlers can be async functions
|
package/__tests__/e2.test.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
const fs = require('fs')
|
|
2
2
|
const path = require('path')
|
|
3
3
|
|
|
4
|
-
// Load
|
|
5
|
-
const
|
|
4
|
+
// Load client.js and custom.js into jsdom
|
|
5
|
+
const clientCode = fs.readFileSync(path.join(__dirname, '../public/client.js'), 'utf8')
|
|
6
|
+
const customCode = fs.readFileSync(path.join(__dirname, '../public/custom.js'), 'utf8')
|
|
6
7
|
|
|
7
|
-
describe('
|
|
8
|
+
describe('client.js', () => {
|
|
8
9
|
beforeEach(() => {
|
|
9
10
|
// Reset DOM
|
|
10
11
|
global.document.body.innerHTML = ''
|
|
@@ -13,80 +14,297 @@ describe('e2.js', () => {
|
|
|
13
14
|
// Clear window.custom
|
|
14
15
|
global.window.custom = {}
|
|
15
16
|
|
|
16
|
-
// Execute
|
|
17
|
-
eval(
|
|
17
|
+
// Execute custom.js first (defines window.custom)
|
|
18
|
+
eval(customCode)
|
|
19
|
+
// Execute client.js code
|
|
20
|
+
eval(clientCode)
|
|
18
21
|
})
|
|
19
22
|
|
|
20
|
-
describe('
|
|
21
|
-
test('
|
|
22
|
-
|
|
23
|
+
describe('$ and $$ selectors', () => {
|
|
24
|
+
test('$ selects single element', () => {
|
|
25
|
+
global.document.body.innerHTML = '<div id="test">Hello</div>'
|
|
26
|
+
expect(window.$('#test').textContent).toBe('Hello')
|
|
27
|
+
})
|
|
23
28
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
expect(
|
|
28
|
-
expect(
|
|
29
|
+
test('$$ selects multiple elements', () => {
|
|
30
|
+
global.document.body.innerHTML = '<div class="item">1</div><div class="item">2</div>'
|
|
31
|
+
const items = window.$$('.item')
|
|
32
|
+
expect(items.length).toBe(2)
|
|
33
|
+
expect(items[0].textContent).toBe('1')
|
|
34
|
+
expect(items[1].textContent).toBe('2')
|
|
29
35
|
})
|
|
30
36
|
|
|
31
|
-
test('
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
test('$ returns null for non-existent element', () => {
|
|
38
|
+
global.document.body.innerHTML = '<div>Test</div>'
|
|
39
|
+
expect(window.$('#nonexistent')).toBeNull()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('$$ returns empty NodeList for non-existent elements', () => {
|
|
43
|
+
global.document.body.innerHTML = '<div>Test</div>'
|
|
44
|
+
expect(window.$$('.nonexistent').length).toBe(0)
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('window API functions', () => {
|
|
49
|
+
let originalFetch
|
|
50
|
+
let originalLocation
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
originalFetch = global.fetch
|
|
54
|
+
global.fetch = jest.fn()
|
|
55
|
+
originalLocation = global.window.location
|
|
56
|
+
delete global.window.location
|
|
57
|
+
global.window.location = { href: '' }
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
global.fetch = originalFetch
|
|
62
|
+
global.window.location = originalLocation
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('window.get makes GET request with encoded key', async () => {
|
|
66
|
+
global.fetch.mockResolvedValueOnce({
|
|
67
|
+
ok: true,
|
|
68
|
+
json: () => Promise.resolve({ value: 'test' })
|
|
36
69
|
})
|
|
37
70
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
expect(
|
|
71
|
+
const result = await window.get('test key')
|
|
72
|
+
|
|
73
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
74
|
+
`${window.api_url}/test%20key`
|
|
75
|
+
)
|
|
76
|
+
expect(result).toEqual({ value: 'test' })
|
|
41
77
|
})
|
|
42
78
|
|
|
43
|
-
test('
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
79
|
+
test('window.set makes POST request with string value', async () => {
|
|
80
|
+
global.fetch.mockResolvedValueOnce({
|
|
81
|
+
ok: true,
|
|
82
|
+
json: () => Promise.resolve({ status: 'saved' })
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const result = await window.set('test', 'value')
|
|
86
|
+
|
|
87
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
88
|
+
`${window.api_url}/test`,
|
|
89
|
+
expect.objectContaining({
|
|
90
|
+
method: 'POST',
|
|
91
|
+
body: 'value'
|
|
92
|
+
})
|
|
93
|
+
)
|
|
94
|
+
expect(result).toEqual({ status: 'saved' })
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('window.set stringifies object values', async () => {
|
|
98
|
+
global.fetch.mockResolvedValueOnce({
|
|
99
|
+
ok: true,
|
|
100
|
+
json: () => Promise.resolve({ status: 'saved' })
|
|
48
101
|
})
|
|
49
102
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
expect(
|
|
103
|
+
await window.set('test', { key: 'value' })
|
|
104
|
+
|
|
105
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
106
|
+
`${window.api_url}/test`,
|
|
107
|
+
expect.objectContaining({
|
|
108
|
+
method: 'POST',
|
|
109
|
+
body: JSON.stringify({ key: 'value' })
|
|
110
|
+
})
|
|
111
|
+
)
|
|
53
112
|
})
|
|
54
113
|
|
|
55
|
-
test('
|
|
56
|
-
|
|
114
|
+
test('window.delete makes DELETE request', async () => {
|
|
115
|
+
global.fetch.mockResolvedValueOnce({
|
|
116
|
+
ok: true,
|
|
117
|
+
json: () => Promise.resolve({ status: 'deleted' })
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
const result = await window.delete('test')
|
|
57
121
|
|
|
58
|
-
expect(
|
|
59
|
-
|
|
122
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
123
|
+
`${window.api_url}/test`,
|
|
124
|
+
expect.objectContaining({
|
|
125
|
+
method: 'DELETE'
|
|
126
|
+
})
|
|
127
|
+
)
|
|
128
|
+
expect(result).toEqual({ status: 'deleted' })
|
|
60
129
|
})
|
|
61
130
|
|
|
62
|
-
test('
|
|
63
|
-
|
|
64
|
-
|
|
131
|
+
test('window.put makes PUT request with string body', async () => {
|
|
132
|
+
global.fetch.mockResolvedValueOnce({
|
|
133
|
+
ok: true,
|
|
134
|
+
json: () => Promise.resolve({ status: 'saved' })
|
|
65
135
|
})
|
|
66
136
|
|
|
67
|
-
|
|
68
|
-
|
|
137
|
+
const result = await window.put('test', 'content')
|
|
138
|
+
|
|
139
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
140
|
+
`${window.api_url}/test`,
|
|
141
|
+
expect.objectContaining({
|
|
142
|
+
method: 'PUT',
|
|
143
|
+
body: 'content'
|
|
144
|
+
})
|
|
145
|
+
)
|
|
146
|
+
expect(result).toEqual({ status: 'saved' })
|
|
69
147
|
})
|
|
70
148
|
|
|
71
|
-
test('
|
|
72
|
-
|
|
149
|
+
test('window.put stringifies object body', async () => {
|
|
150
|
+
global.fetch.mockResolvedValueOnce({
|
|
151
|
+
ok: true,
|
|
152
|
+
json: () => Promise.resolve({ status: 'saved' })
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
await window.put('test', { data: 'value' })
|
|
73
156
|
|
|
74
|
-
expect(
|
|
75
|
-
|
|
76
|
-
|
|
157
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
158
|
+
`${window.api_url}/test`,
|
|
159
|
+
expect.objectContaining({
|
|
160
|
+
method: 'PUT',
|
|
161
|
+
body: JSON.stringify({ data: 'value' })
|
|
162
|
+
})
|
|
163
|
+
)
|
|
77
164
|
})
|
|
78
165
|
|
|
79
|
-
test('handles
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
166
|
+
test('window.put handles Blob body', async () => {
|
|
167
|
+
const blob = new Blob(['test'], { type: 'text/plain' })
|
|
168
|
+
global.fetch.mockResolvedValueOnce({
|
|
169
|
+
ok: true,
|
|
170
|
+
json: () => Promise.resolve({ status: 'saved' })
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
await window.put('test', blob)
|
|
174
|
+
|
|
175
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
176
|
+
`${window.api_url}/test`,
|
|
177
|
+
expect.objectContaining({
|
|
178
|
+
method: 'PUT',
|
|
179
|
+
body: blob
|
|
180
|
+
})
|
|
181
|
+
)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
test('window.purge makes PURGE request', async () => {
|
|
185
|
+
global.fetch.mockResolvedValueOnce({
|
|
186
|
+
ok: true,
|
|
187
|
+
json: () => Promise.resolve({ status: 'deleted' })
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
const result = await window.purge('test')
|
|
191
|
+
|
|
192
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
193
|
+
`${window.api_url}/test`,
|
|
194
|
+
expect.objectContaining({
|
|
195
|
+
method: 'PURGE'
|
|
196
|
+
})
|
|
197
|
+
)
|
|
198
|
+
expect(result).toEqual({ status: 'deleted' })
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
test('window.list makes PROPFIND request to base URL', async () => {
|
|
202
|
+
global.fetch.mockResolvedValueOnce({
|
|
203
|
+
ok: true,
|
|
204
|
+
json: () => Promise.resolve([{ name: 'file1' }])
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
const result = await window.list()
|
|
208
|
+
|
|
209
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
210
|
+
window.api_url,
|
|
211
|
+
expect.objectContaining({
|
|
212
|
+
method: 'PROPFIND'
|
|
213
|
+
})
|
|
214
|
+
)
|
|
215
|
+
expect(result).toEqual([{ name: 'file1' }])
|
|
84
216
|
})
|
|
85
217
|
|
|
86
|
-
test('
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
218
|
+
test('window.login redirects to /login', () => {
|
|
219
|
+
window.login()
|
|
220
|
+
expect(global.window.location.href).toBe(`${window.api_url}/login`)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
test('window.logout redirects to /logout', () => {
|
|
224
|
+
window.logout()
|
|
225
|
+
expect(global.window.location.href).toBe(`${window.api_url}/logout`)
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
describe('window.custom components', () => {
|
|
230
|
+
test('hello-world component is a function', () => {
|
|
231
|
+
expect(typeof window.custom['hello-world']).toBe('function')
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
test('hello-world component renders correctly', () => {
|
|
235
|
+
const result = window.custom['hello-world']('Test')
|
|
236
|
+
expect(result).toBe('Hello Test')
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
test('hello-world-2 component has prop and render methods', () => {
|
|
240
|
+
expect(window.custom['hello-world-2']).toBeDefined()
|
|
241
|
+
expect(typeof window.custom['hello-world-2'].prop).toBe('function')
|
|
242
|
+
expect(typeof window.custom['hello-world-2'].render).toBe('function')
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
test('hello-world-2 component prop works', () => {
|
|
246
|
+
const result = window.custom['hello-world-2'].prop('Test')
|
|
247
|
+
expect(result).toBe('Test World')
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
test('hello-world-2 component render works', () => {
|
|
251
|
+
const result = window.custom['hello-world-2'].render('Test')
|
|
252
|
+
expect(result).toBe('Test World')
|
|
253
|
+
})
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
describe('window.state proxy', () => {
|
|
258
|
+
test('state.set updates DOM elements with data attribute', () => {
|
|
259
|
+
global.document.body.innerHTML = '<hello-world data="name"></hello-world>'
|
|
260
|
+
|
|
261
|
+
window.state.name = 'John'
|
|
262
|
+
|
|
263
|
+
const el = global.document.querySelector('hello-world')
|
|
264
|
+
expect(el.innerHTML).toBe('Hello John')
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
test('state.set updates multiple elements with same data attribute', () => {
|
|
268
|
+
global.document.body.innerHTML = '<hello-world data="name"></hello-world><hello-world data="name"></hello-world>'
|
|
269
|
+
|
|
270
|
+
window.state.name = 'Jane'
|
|
271
|
+
|
|
272
|
+
const els = global.document.querySelectorAll('hello-world')
|
|
273
|
+
expect(els[0].innerHTML).toBe('Hello Jane')
|
|
274
|
+
expect(els[1].innerHTML).toBe('Hello Jane')
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
test('state.set works with object components', () => {
|
|
278
|
+
global.document.body.innerHTML = '<hello-world-2 data="test"></hello-world-2>'
|
|
279
|
+
|
|
280
|
+
window.state.test = 'Hello'
|
|
281
|
+
|
|
282
|
+
const el = global.document.querySelector('hello-world-2')
|
|
283
|
+
expect(el.innerHTML).toBe('Hello World')
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
test('state.set stores value in proxy object', () => {
|
|
287
|
+
window.state.test = 'value'
|
|
288
|
+
expect(window.state.test).toBe('value')
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
test('state.set handles multiple properties', () => {
|
|
292
|
+
global.document.body.innerHTML = '<hello-world data="a"></hello-world><hello-world data="b"></hello-world>'
|
|
293
|
+
|
|
294
|
+
window.state.a = 'A'
|
|
295
|
+
window.state.b = 'B'
|
|
296
|
+
|
|
297
|
+
expect(global.document.querySelector('[data="a"]').innerHTML).toBe('Hello A')
|
|
298
|
+
expect(global.document.querySelector('[data="b"]').innerHTML).toBe('Hello B')
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
test('state.set does not update elements without matching data attribute', () => {
|
|
302
|
+
global.document.body.innerHTML = '<hello-world data="name"></hello-world><div data="other">Original</div>'
|
|
303
|
+
|
|
304
|
+
window.state.name = 'John'
|
|
305
|
+
|
|
306
|
+
expect(global.document.querySelector('[data="name"]').innerHTML).toBe('Hello John')
|
|
307
|
+
expect(global.document.querySelector('[data="other"]').innerHTML).toBe('Original')
|
|
90
308
|
})
|
|
91
309
|
})
|
|
92
310
|
})
|