ripple 0.2.115 → 0.2.118

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 (93) hide show
  1. package/package.json +16 -16
  2. package/src/compiler/index.js +20 -1
  3. package/src/compiler/phases/1-parse/index.js +79 -0
  4. package/src/compiler/phases/3-transform/client/index.js +54 -8
  5. package/src/compiler/phases/3-transform/segments.js +107 -60
  6. package/src/compiler/phases/3-transform/server/index.js +21 -11
  7. package/src/compiler/types/index.d.ts +16 -0
  8. package/src/runtime/index-client.js +19 -185
  9. package/src/runtime/index-server.js +24 -0
  10. package/src/runtime/internal/client/bindings.js +443 -0
  11. package/src/runtime/internal/client/index.js +4 -0
  12. package/src/runtime/internal/client/runtime.js +10 -0
  13. package/src/runtime/internal/client/utils.js +0 -8
  14. package/src/runtime/map.js +11 -1
  15. package/src/runtime/set.js +11 -1
  16. package/tests/client/__snapshots__/for.test.ripple.snap +80 -0
  17. package/tests/client/_etc.test.ripple +5 -0
  18. package/tests/client/array/array.copy-within.test.ripple +120 -0
  19. package/tests/client/array/array.derived.test.ripple +495 -0
  20. package/tests/client/array/array.iteration.test.ripple +115 -0
  21. package/tests/client/array/array.mutations.test.ripple +385 -0
  22. package/tests/client/array/array.static.test.ripple +237 -0
  23. package/tests/client/array/array.to-methods.test.ripple +93 -0
  24. package/tests/client/basic/__snapshots__/basic.attributes.test.ripple.snap +60 -0
  25. package/tests/client/basic/__snapshots__/basic.rendering.test.ripple.snap +106 -0
  26. package/tests/client/basic/__snapshots__/basic.text.test.ripple.snap +49 -0
  27. package/tests/client/basic/basic.attributes.test.ripple +474 -0
  28. package/tests/client/basic/basic.collections.test.ripple +94 -0
  29. package/tests/client/basic/basic.components.test.ripple +225 -0
  30. package/tests/client/basic/basic.errors.test.ripple +126 -0
  31. package/tests/client/basic/basic.events.test.ripple +222 -0
  32. package/tests/client/basic/basic.reactivity.test.ripple +476 -0
  33. package/tests/client/basic/basic.rendering.test.ripple +204 -0
  34. package/tests/client/basic/basic.styling.test.ripple +63 -0
  35. package/tests/client/basic/basic.utilities.test.ripple +25 -0
  36. package/tests/client/boundaries.test.ripple +2 -21
  37. package/tests/client/compiler/__snapshots__/compiler.assignments.test.ripple.snap +12 -0
  38. package/tests/client/compiler/__snapshots__/compiler.typescript.test.ripple.snap +22 -0
  39. package/tests/client/compiler/compiler.assignments.test.ripple +112 -0
  40. package/tests/client/compiler/compiler.attributes.test.ripple +95 -0
  41. package/tests/client/compiler/compiler.basic.test.ripple +203 -0
  42. package/tests/client/compiler/compiler.regex.test.ripple +87 -0
  43. package/tests/client/compiler/compiler.typescript.test.ripple +29 -0
  44. package/tests/client/{__snapshots__/composite.test.ripple.snap → composite/__snapshots__/composite.render.test.ripple.snap} +2 -2
  45. package/tests/client/composite/composite.dynamic-components.test.ripple +100 -0
  46. package/tests/client/composite/composite.generics.test.ripple +211 -0
  47. package/tests/client/composite/composite.props.test.ripple +106 -0
  48. package/tests/client/composite/composite.reactivity.test.ripple +184 -0
  49. package/tests/client/composite/composite.render.test.ripple +84 -0
  50. package/tests/client/computed-properties.test.ripple +2 -21
  51. package/tests/client/context.test.ripple +5 -22
  52. package/tests/client/date.test.ripple +1 -20
  53. package/tests/client/dynamic-elements.test.ripple +16 -24
  54. package/tests/client/for.test.ripple +4 -23
  55. package/tests/client/head.test.ripple +11 -23
  56. package/tests/client/html.test.ripple +1 -20
  57. package/tests/client/input-value.test.ripple +11 -31
  58. package/tests/client/map.test.ripple +82 -20
  59. package/tests/client/media-query.test.ripple +10 -23
  60. package/tests/client/object.test.ripple +5 -24
  61. package/tests/client/portal.test.ripple +2 -19
  62. package/tests/client/ref.test.ripple +8 -26
  63. package/tests/client/set.test.ripple +84 -22
  64. package/tests/client/svg.test.ripple +1 -22
  65. package/tests/client/switch.test.ripple +6 -25
  66. package/tests/client/tracked-expression.test.ripple +2 -21
  67. package/tests/client/typescript-generics.test.ripple +0 -21
  68. package/tests/client/url/url.derived.test.ripple +83 -0
  69. package/tests/client/url/url.parsing.test.ripple +165 -0
  70. package/tests/client/url/url.partial-removal.test.ripple +198 -0
  71. package/tests/client/url/url.reactivity.test.ripple +449 -0
  72. package/tests/client/url/url.serialization.test.ripple +50 -0
  73. package/tests/client/url-search-params/url-search-params.derived.test.ripple +84 -0
  74. package/tests/client/url-search-params/url-search-params.initialization.test.ripple +61 -0
  75. package/tests/client/url-search-params/url-search-params.iteration.test.ripple +153 -0
  76. package/tests/client/url-search-params/url-search-params.mutation.test.ripple +343 -0
  77. package/tests/client/url-search-params/url-search-params.retrieval.test.ripple +160 -0
  78. package/tests/client/url-search-params/url-search-params.serialization.test.ripple +53 -0
  79. package/tests/client/url-search-params/url-search-params.tracked-url.test.ripple +55 -0
  80. package/tests/client.d.ts +12 -0
  81. package/tests/server/if.test.ripple +66 -0
  82. package/tests/setup-client.js +28 -0
  83. package/tsconfig.json +4 -2
  84. package/types/index.d.ts +92 -46
  85. package/LICENSE +0 -21
  86. package/tests/client/__snapshots__/basic.test.ripple.snap +0 -117
  87. package/tests/client/__snapshots__/compiler.test.ripple.snap +0 -33
  88. package/tests/client/array.test.ripple +0 -1455
  89. package/tests/client/basic.test.ripple +0 -1892
  90. package/tests/client/compiler.test.ripple +0 -541
  91. package/tests/client/composite.test.ripple +0 -692
  92. package/tests/client/url-search-params.test.ripple +0 -912
  93. package/tests/client/url.test.ripple +0 -954
@@ -1,29 +1,10 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { mount, flushSync, TrackedObject, track } from 'ripple';
1
+ import { flushSync, TrackedObject } from 'ripple';
3
2
  import { TRACKED_OBJECT } from '../../src/runtime/internal/client/constants.js';
4
3
 
5
4
  describe('TrackedObject', () => {
6
- let container;
7
-
8
- function render(component) {
9
- mount(component, {
10
- target: container
11
- });
12
- }
13
-
14
- beforeEach(() => {
15
- container = document.createElement('div');
16
- document.body.appendChild(container);
17
- });
18
-
19
- afterEach(() => {
20
- document.body.removeChild(container);
21
- container = null;
22
- });
23
-
24
5
  it('makes new properties reactive', () => {
25
6
  component ObjectTest() {
26
- const obj = new TrackedObject({});
7
+ const obj = new TrackedObject<{ a: number; }>({});
27
8
 
28
9
  obj.a = 0;
29
10
 
@@ -67,7 +48,7 @@ describe('TrackedObject', () => {
67
48
 
68
49
  it('checks if property exists via the has trap', () => {
69
50
  component ObjectTest() {
70
- const obj = new TrackedObject({b: 1});
51
+ const obj = new TrackedObject<{ a: number; b: number; }>({b: 1});
71
52
 
72
53
  obj.a = 0;
73
54
 
@@ -83,7 +64,7 @@ describe('TrackedObject', () => {
83
64
 
84
65
  it('deletes properties via the delete trap', () => {
85
66
  component ObjectTest() {
86
- const obj = new TrackedObject({a: 0, b: 1});
67
+ const obj = new TrackedObject<{ a?: number; b: number; }>({a: 0, b: 1});
87
68
 
88
69
  <pre>{String(obj.a)}</pre>
89
70
  <button onClick={() => { delete obj.a; }}>{'Delete A'}</button>
@@ -104,7 +85,7 @@ describe('TrackedObject', () => {
104
85
 
105
86
  it('checks if non-existent property is reactive when added later', () => {
106
87
  component ObjectTest() {
107
- const obj = new TrackedObject({});
88
+ const obj = new TrackedObject<{ a?: number; }>({});
108
89
 
109
90
  <pre>{String(obj.a)} </pre>
110
91
  <button onClick={() => { obj.a = 1; }}>{'Add A'}</button>
@@ -1,24 +1,7 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { mount, Portal, track, flushSync } from 'ripple';
1
+ import { Portal, track, flushSync } from 'ripple';
3
2
 
4
3
  describe('Portal', () => {
5
- let container;
6
-
7
- function render(component) {
8
- mount(component, {
9
- target: container,
10
- });
11
- }
12
-
13
- beforeEach(() => {
14
- container = document.createElement('div');
15
- document.body.appendChild(container);
16
- });
17
-
18
4
  afterEach(() => {
19
- // Remove container
20
- document.body.removeChild(container);
21
-
22
5
  // Clean up any leftover portal content from document.body
23
6
  const portals = document.body.querySelectorAll('.test-portal');
24
7
  portals.forEach(el => el.remove());
@@ -164,4 +147,4 @@ describe('Portal', () => {
164
147
 
165
148
  expect(portalElement.textContent).toContain('Count: 1');
166
149
  });
167
- });
150
+ });
@@ -1,50 +1,32 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { mount, flushSync, TrackedArray, track, createRefKey } from 'ripple';
1
+ import { describe, it, expect } from 'vitest';
2
+ import { flushSync, TrackedArray, track, createRefKey } from 'ripple';
3
3
 
4
4
  describe('refs', () => {
5
- let container;
6
-
7
- function render(component) {
8
- mount(component, {
9
- target: container
10
- });
11
- }
12
-
13
- beforeEach(() => {
14
- container = document.createElement('div');
15
- document.body.appendChild(container);
16
- });
17
-
18
- afterEach(() => {
19
- document.body.removeChild(container);
20
- container = null;
21
- });
22
-
23
5
  it('capture a <div>', () => {
24
- let div;
6
+ let div: HTMLDivElement | undefined;
25
7
 
26
8
  component Component() {
27
- <div {ref (node) => { div = node; }}>{'Hello World'}</div>
9
+ <div {ref (node: HTMLDivElement) => { div = node; }}>{'Hello World'}</div>
28
10
  }
29
11
  render(Component);
30
12
  flushSync();
31
- expect(div.textContent).toBe('Hello World');
13
+ expect(div?.textContent).toBe('Hello World');
32
14
  });
33
15
 
34
16
  it('works with spreading from composite component', () => {
35
- let _node;
17
+ let _node: Child | undefined;
36
18
 
37
19
  component Component() {
38
20
  let items = TrackedArray.from([1, 2, 3]);
39
21
 
40
- function componentRef(node) {
22
+ function componentRef(node: Child) {
41
23
  _node = node;
42
24
  }
43
25
 
44
26
  <Child {ref componentRef} {items} />
45
27
  }
46
28
 
47
- component Child(props) {
29
+ component Child(props: { items: TrackedArray<number> }) {
48
30
  const { items, ...rest } = props;
49
31
  <pre {...rest}>{JSON.stringify(items)}</pre>
50
32
  <pre>{items.length}</pre>
@@ -1,25 +1,6 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { mount, flushSync, TrackedSet, track } from 'ripple';
1
+ import { flushSync, TrackedSet, track } from 'ripple';
3
2
 
4
3
  describe('TrackedSet', () => {
5
- let container;
6
-
7
- function render(component) {
8
- mount(component, {
9
- target: container
10
- });
11
- }
12
-
13
- beforeEach(() => {
14
- container = document.createElement('div');
15
- document.body.appendChild(container);
16
- });
17
-
18
- afterEach(() => {
19
- document.body.removeChild(container);
20
- container = null;
21
- });
22
-
23
4
  it('handles add and delete operations', () => {
24
5
  component SetTest() {
25
6
  let items = new TrackedSet([1, 2, 3]);
@@ -29,7 +10,7 @@ describe('TrackedSet', () => {
29
10
  <Child items={items} />
30
11
  }
31
12
 
32
- component Child({ items }) {
13
+ component Child({ items }: { items: TrackedSet<number> }) {
33
14
  <pre>{JSON.stringify(items)}</pre>
34
15
  <pre>{items.size}</pre>
35
16
  }
@@ -60,7 +41,7 @@ describe('TrackedSet', () => {
60
41
  <Child items={items} />
61
42
  }
62
43
 
63
- component Child({ items }) {
44
+ component Child({ items }: { items: TrackedSet<number> }) {
64
45
  <pre>{JSON.stringify(items)}</pre>
65
46
  <pre>{items.size}</pre>
66
47
  }
@@ -96,4 +77,85 @@ describe('TrackedSet', () => {
96
77
 
97
78
  expect(container.querySelectorAll('pre')[0].textContent).toBe('false');
98
79
  });
80
+
81
+ it('creates empty TrackedSet using #Set() shorthand syntax', () => {
82
+ component SetTest() {
83
+ let items = #Set();
84
+
85
+ <button onClick={() => items.add(1)}>{'add'}</button>
86
+ <pre>{items.size}</pre>
87
+ }
88
+
89
+ render(SetTest);
90
+
91
+ expect(container.querySelector('pre').textContent).toBe('0');
92
+
93
+ const addButton = container.querySelector('button');
94
+ addButton.click();
95
+ flushSync();
96
+
97
+ expect(container.querySelector('pre').textContent).toBe('1');
98
+ });
99
+
100
+ it('creates TrackedSet with initial values using #Set() shorthand syntax', () => {
101
+ component SetTest() {
102
+ let items = #Set([1, 2, 3, 4]);
103
+ let hasValue = track(() => items.has(3));
104
+
105
+ <button onClick={() => items.delete(3)}>{'delete'}</button>
106
+ <pre>{items.size}</pre>
107
+ <pre>{@hasValue}</pre>
108
+ }
109
+
110
+ render(SetTest);
111
+
112
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('4');
113
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('true');
114
+
115
+ const deleteButton = container.querySelector('button');
116
+ deleteButton.click();
117
+ flushSync();
118
+
119
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('3');
120
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('false');
121
+ });
122
+
123
+ it('handles all operations with #Set() shorthand syntax', () => {
124
+ component SetTest() {
125
+ let items = #Set([10, 20, 30]);
126
+ let values = track(() => Array.from(items.values()));
127
+
128
+ <button onClick={() => items.add(40)}>{'add'}</button>
129
+ <button onClick={() => items.delete(20)}>{'delete'}</button>
130
+ <button onClick={() => items.clear()}>{'clear'}</button>
131
+
132
+ <pre>{JSON.stringify(@values)}</pre>
133
+ <pre>{items.size}</pre>
134
+ }
135
+
136
+ render(SetTest);
137
+
138
+ const [addButton, deleteButton, clearButton] = container.querySelectorAll('button');
139
+
140
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('[10,20,30]');
141
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('3');
142
+
143
+ addButton.click();
144
+ flushSync();
145
+
146
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('[10,20,30,40]');
147
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('4');
148
+
149
+ deleteButton.click();
150
+ flushSync();
151
+
152
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('[10,30,40]');
153
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('3');
154
+
155
+ clearButton.click();
156
+ flushSync();
157
+
158
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('[]');
159
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('0');
160
+ });
99
161
  });
@@ -1,25 +1,4 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { mount, flushSync } from 'ripple';
3
-
4
1
  describe('SVG namespace handling', () => {
5
- let container;
6
-
7
- function render(component) {
8
- mount(component, {
9
- target: container
10
- });
11
- }
12
-
13
- beforeEach(() => {
14
- container = document.createElement('div');
15
- document.body.appendChild(container);
16
- });
17
-
18
- afterEach(() => {
19
- document.body.removeChild(container);
20
- container = null;
21
- });
22
-
23
2
  it('should render static SVG elements with correct namespace', () => {
24
3
  component App() {
25
4
  <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" stroke="currentColor">
@@ -143,7 +122,7 @@ describe('SVG namespace handling', () => {
143
122
  expect(svg.namespaceURI).toBe('http://www.w3.org/2000/svg');
144
123
  expect(g.namespaceURI).toBe('http://www.w3.org/2000/svg');
145
124
  expect(rects.length).toBe(2);
146
-
125
+
147
126
  rects.forEach(rect => {
148
127
  expect(rect.namespaceURI).toBe('http://www.w3.org/2000/svg');
149
128
  });
@@ -1,25 +1,6 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { mount, flushSync, track } from 'ripple';
1
+ import { flushSync, track } from 'ripple';
3
2
 
4
3
  describe('switch statements', () => {
5
- let container;
6
-
7
- function render(component) {
8
- mount(component, {
9
- target: container
10
- });
11
- }
12
-
13
- beforeEach(() => {
14
- container = document.createElement('div');
15
- document.body.appendChild(container);
16
- });
17
-
18
- afterEach(() => {
19
- document.body.removeChild(container);
20
- container = null;
21
- });
22
-
23
4
  it('renders simple switch with literal cases', () => {
24
5
  component App() {
25
6
  let value = track('b');
@@ -115,19 +96,19 @@ describe('switch statements', () => {
115
96
 
116
97
  switch (@status) {
117
98
  case 'active':
118
- message = 'Currently active.';
99
+ @message = 'Currently active.';
119
100
  <div>{'Status: ' + @message}</div>
120
101
  break;
121
102
  case 'pending':
122
- message = 'Waiting for completion.';
103
+ @message = 'Waiting for completion.';
123
104
  <div>{'Status: ' + @message}</div>
124
105
  break;
125
106
  case 'completed':
126
- message = 'Task finished!';
107
+ @message = 'Task finished!';
127
108
  <div class="success">{'Status: ' + @message}</div>
128
109
  break;
129
110
  default:
130
- message = 'An error occurred.';
111
+ @message = 'An error occurred.';
131
112
  <div class="error">{'Status: ' + @message}</div>
132
113
  }
133
114
  }
@@ -149,4 +130,4 @@ describe('switch statements', () => {
149
130
  expect(container.textContent).toBe('PendingCompletedErrorStatus: An error occurred.');
150
131
  expect(container.querySelector('.error')).toBeTruthy();
151
132
  });
152
- });
133
+ });
@@ -1,25 +1,6 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { mount, flushSync, TrackedSet, track } from 'ripple';
1
+ import { track } from 'ripple';
3
2
 
4
3
  describe('TrackedExpression tests', () => {
5
- let container;
6
-
7
- function render(component) {
8
- mount(component, {
9
- target: container
10
- });
11
- }
12
-
13
- beforeEach(() => {
14
- container = document.createElement('div');
15
- document.body.appendChild(container);
16
- });
17
-
18
- afterEach(() => {
19
- document.body.removeChild(container);
20
- container = null;
21
- });
22
-
23
4
  it('should handle the syntax correctly', () => {
24
5
  component App() {
25
6
  let count = track(0);
@@ -43,4 +24,4 @@ describe('TrackedExpression tests', () => {
43
24
  render(App);
44
25
  expect(container).toMatchSnapshot();
45
26
  });
46
- });
27
+ });
@@ -1,25 +1,4 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { mount } from 'ripple';
3
-
4
1
  describe('generic patterns', () => {
5
- let container;
6
-
7
- function render(component) {
8
- mount(component, {
9
- target: container
10
- });
11
- }
12
-
13
- beforeEach(() => {
14
- container = document.createElement('div');
15
- document.body.appendChild(container);
16
- });
17
-
18
- afterEach(() => {
19
- document.body.removeChild(container);
20
- container = null;
21
- });
22
-
23
2
  it('tests simple generic function', () => {
24
3
  component App() {
25
4
  const e = new Map<string, Promise<number>>();
@@ -0,0 +1,83 @@
1
+ import { track, flushSync, TrackedURL } from 'ripple';
2
+
3
+ describe('TrackedURL > derived', () => {
4
+ it('handles reactive computed properties based on URL', () => {
5
+ component URLTest() {
6
+ const url = new TrackedURL('https://example.com/users/123?tab=profile');
7
+ let userId = track(() => url.pathname.split('/').pop());
8
+ let activeTab = track(() => url.searchParams.get('tab'));
9
+
10
+ <button onClick={() => url.pathname = '/users/456'}>{'Change User'}</button>
11
+ <button onClick={() => url.searchParams.set('tab', 'settings')}>{'Change Tab'}</button>
12
+ <pre>{`User ID: ${@userId}`}</pre>
13
+ <pre>{`Active Tab: ${@activeTab}`}</pre>
14
+ }
15
+
16
+ render(URLTest);
17
+
18
+ const changeUserBtn = container.querySelectorAll('button')[0];
19
+ const changeTabBtn = container.querySelectorAll('button')[1];
20
+
21
+ // Initial state
22
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('User ID: 123');
23
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('Active Tab: profile');
24
+
25
+ // Change user
26
+ changeUserBtn.click();
27
+ flushSync();
28
+
29
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('User ID: 456');
30
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('Active Tab: profile');
31
+
32
+ // Change tab
33
+ changeTabBtn.click();
34
+ flushSync();
35
+
36
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('User ID: 456');
37
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('Active Tab: settings');
38
+ });
39
+
40
+ it('maintains reactivity across multiple components', () => {
41
+ component ParentTest() {
42
+ const url = new TrackedURL('https://example.com/path?count=0');
43
+
44
+ <ChildA url={url} />
45
+ <ChildB url={url} />
46
+ }
47
+
48
+ component ChildA({ url }: { url: TrackedURL }) {
49
+ <button onClick={() => {
50
+ const current = parseInt(url.searchParams.get('count') || '0', 10);
51
+ url.searchParams.set('count', String(current + 1));
52
+ }}>{'Increment Count'}</button>
53
+ }
54
+
55
+ component ChildB({ url }: { url: TrackedURL }) {
56
+ let count = track(() => url.searchParams.get('count'));
57
+
58
+ <pre>{url.href}</pre>
59
+ <pre>{@count}</pre>
60
+ }
61
+
62
+ render(ParentTest);
63
+
64
+ const button = container.querySelector('button');
65
+
66
+ // Initial state
67
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path?count=0');
68
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('0');
69
+
70
+ // Increment from child
71
+ button.click();
72
+ flushSync();
73
+
74
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path?count=1');
75
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('1');
76
+
77
+ button.click();
78
+ flushSync();
79
+
80
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path?count=2');
81
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('2');
82
+ });
83
+ });
@@ -0,0 +1,165 @@
1
+ import { flushSync, TrackedURL } from 'ripple';
2
+
3
+ describe('TrackedURL > parsing', () => {
4
+ it('creates URL from string with reactivity', () => {
5
+ component URLTest() {
6
+ const url = new TrackedURL('https://example.com:8080/path?foo=bar#section');
7
+
8
+ <pre>{url.href}</pre>
9
+ <pre>{url.protocol}</pre>
10
+ <pre>{url.hostname}</pre>
11
+ <pre>{url.port}</pre>
12
+ <pre>{url.pathname}</pre>
13
+ <pre>{url.search}</pre>
14
+ <pre>{url.hash}</pre>
15
+ }
16
+
17
+ render(URLTest);
18
+
19
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com:8080/path?foo=bar#section');
20
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('https:');
21
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('example.com');
22
+ expect(container.querySelectorAll('pre')[3].textContent).toBe('8080');
23
+ expect(container.querySelectorAll('pre')[4].textContent).toBe('/path');
24
+ expect(container.querySelectorAll('pre')[5].textContent).toBe('?foo=bar');
25
+ expect(container.querySelectorAll('pre')[6].textContent).toBe('#section');
26
+ });
27
+
28
+ it('creates URL from string with base URL', () => {
29
+ component URLTest() {
30
+ const url = new TrackedURL('/path?query=value', 'https://example.com');
31
+
32
+ <pre>{url.href}</pre>
33
+ <pre>{url.origin}</pre>
34
+ }
35
+
36
+ render(URLTest);
37
+
38
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path?query=value');
39
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('https://example.com');
40
+ });
41
+
42
+ it('handles URL encoding correctly', () => {
43
+ component URLTest() {
44
+ const url = new TrackedURL('https://example.com/path with spaces?key=value with spaces');
45
+
46
+ <pre>{url.pathname}</pre>
47
+ <pre>{url.search}</pre>
48
+ <pre>{url.href}</pre>
49
+ }
50
+
51
+ render(URLTest);
52
+
53
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('/path%20with%20spaces');
54
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('?key=value%20with%20spaces');
55
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('https://example.com/path%20with%20spaces?key=value%20with%20spaces');
56
+ });
57
+
58
+ it('handles URL with file protocol', () => {
59
+ component URLTest() {
60
+ const url = new TrackedURL('file:///Users/username/documents/file.txt');
61
+
62
+ <pre>{url.protocol}</pre>
63
+ <pre>{url.pathname}</pre>
64
+ <pre>{url.href}</pre>
65
+ }
66
+
67
+ render(URLTest);
68
+
69
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('file:');
70
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('/Users/username/documents/file.txt');
71
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('file:///Users/username/documents/file.txt');
72
+ });
73
+
74
+ it('handles URL with IPv4 address', () => {
75
+ component URLTest() {
76
+ const url = new TrackedURL('https://192.168.1.1:8080/path');
77
+
78
+ <button onClick={() => url.hostname = '10.0.0.1'}>{'Change IP'}</button>
79
+ <pre>{url.href}</pre>
80
+ <pre>{url.hostname}</pre>
81
+ }
82
+
83
+ render(URLTest);
84
+
85
+ const button = container.querySelector('button');
86
+
87
+ // Initial state
88
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://192.168.1.1:8080/path');
89
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('192.168.1.1');
90
+
91
+ // Change IP
92
+ button.click();
93
+ flushSync();
94
+
95
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://10.0.0.1:8080/path');
96
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('10.0.0.1');
97
+ });
98
+
99
+ it('handles URL with localhost', () => {
100
+ component URLTest() {
101
+ const url = new TrackedURL('http://localhost:3000/api/data');
102
+
103
+ <button onClick={() => url.port = '8080'}>{'Change Port'}</button>
104
+ <pre>{url.href}</pre>
105
+ <pre>{url.hostname}</pre>
106
+ <pre>{url.port}</pre>
107
+ }
108
+
109
+ render(URLTest);
110
+
111
+ const button = container.querySelector('button');
112
+
113
+ // Initial state
114
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('http://localhost:3000/api/data');
115
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('localhost');
116
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('3000');
117
+
118
+ // Change port
119
+ button.click();
120
+ flushSync();
121
+
122
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('http://localhost:8080/api/data');
123
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('localhost');
124
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('8080');
125
+ });
126
+
127
+ it('handles URL with multiple path segments', () => {
128
+ component URLTest() {
129
+ const url = new TrackedURL('https://example.com/api/v1/users/123/profile');
130
+
131
+ <button onClick={() => url.pathname = '/api/v2/users/456/settings'}>{'Change Path'}</button>
132
+ <pre>{url.pathname}</pre>
133
+ <pre>{url.href}</pre>
134
+ }
135
+
136
+ render(URLTest);
137
+
138
+ const button = container.querySelector('button');
139
+
140
+ // Initial state
141
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('/api/v1/users/123/profile');
142
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('https://example.com/api/v1/users/123/profile');
143
+
144
+ // Change path
145
+ button.click();
146
+ flushSync();
147
+
148
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('/api/v2/users/456/settings');
149
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('https://example.com/api/v2/users/456/settings');
150
+ });
151
+
152
+ it('handles relative URL paths correctly', () => {
153
+ component URLTest() {
154
+ const url = new TrackedURL('../sibling/path', 'https://example.com/parent/current');
155
+
156
+ <pre>{url.href}</pre>
157
+ <pre>{url.pathname}</pre>
158
+ }
159
+
160
+ render(URLTest);
161
+
162
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/sibling/path');
163
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('/sibling/path');
164
+ });
165
+ });