ripple 0.2.116 → 0.2.119

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 (86) hide show
  1. package/package.json +16 -16
  2. package/src/compiler/index.js +20 -1
  3. package/src/compiler/phases/3-transform/segments.js +99 -57
  4. package/src/compiler/phases/3-transform/server/index.js +21 -11
  5. package/src/runtime/index-client.js +1 -1
  6. package/src/runtime/index-server.js +24 -0
  7. package/src/runtime/internal/client/runtime.js +10 -0
  8. package/src/runtime/internal/client/utils.js +0 -8
  9. package/tests/client/__snapshots__/for.test.ripple.snap +80 -0
  10. package/tests/client/_etc.test.ripple +5 -0
  11. package/tests/client/array/array.copy-within.test.ripple +120 -0
  12. package/tests/client/array/array.derived.test.ripple +495 -0
  13. package/tests/client/array/array.iteration.test.ripple +115 -0
  14. package/tests/client/array/array.mutations.test.ripple +385 -0
  15. package/tests/client/array/array.static.test.ripple +237 -0
  16. package/tests/client/array/array.to-methods.test.ripple +93 -0
  17. package/tests/client/basic/__snapshots__/basic.attributes.test.ripple.snap +60 -0
  18. package/tests/client/basic/__snapshots__/basic.rendering.test.ripple.snap +106 -0
  19. package/tests/client/basic/__snapshots__/basic.text.test.ripple.snap +49 -0
  20. package/tests/client/basic/basic.attributes.test.ripple +474 -0
  21. package/tests/client/basic/basic.collections.test.ripple +94 -0
  22. package/tests/client/basic/basic.components.test.ripple +225 -0
  23. package/tests/client/basic/basic.errors.test.ripple +126 -0
  24. package/tests/client/basic/basic.events.test.ripple +222 -0
  25. package/tests/client/basic/basic.reactivity.test.ripple +476 -0
  26. package/tests/client/basic/basic.rendering.test.ripple +204 -0
  27. package/tests/client/basic/basic.styling.test.ripple +63 -0
  28. package/tests/client/basic/basic.utilities.test.ripple +25 -0
  29. package/tests/client/boundaries.test.ripple +2 -21
  30. package/tests/client/compiler/__snapshots__/compiler.assignments.test.ripple.snap +12 -0
  31. package/tests/client/compiler/__snapshots__/compiler.typescript.test.ripple.snap +22 -0
  32. package/tests/client/compiler/compiler.assignments.test.ripple +112 -0
  33. package/tests/client/compiler/compiler.attributes.test.ripple +95 -0
  34. package/tests/client/compiler/compiler.basic.test.ripple +203 -0
  35. package/tests/client/compiler/compiler.regex.test.ripple +87 -0
  36. package/tests/client/compiler/compiler.typescript.test.ripple +29 -0
  37. package/tests/client/{__snapshots__/composite.test.ripple.snap → composite/__snapshots__/composite.render.test.ripple.snap} +2 -2
  38. package/tests/client/composite/composite.dynamic-components.test.ripple +100 -0
  39. package/tests/client/composite/composite.generics.test.ripple +211 -0
  40. package/tests/client/composite/composite.props.test.ripple +106 -0
  41. package/tests/client/composite/composite.reactivity.test.ripple +184 -0
  42. package/tests/client/composite/composite.render.test.ripple +84 -0
  43. package/tests/client/computed-properties.test.ripple +2 -21
  44. package/tests/client/context.test.ripple +5 -22
  45. package/tests/client/date.test.ripple +1 -20
  46. package/tests/client/dynamic-elements.test.ripple +16 -24
  47. package/tests/client/for.test.ripple +4 -23
  48. package/tests/client/head.test.ripple +11 -23
  49. package/tests/client/html.test.ripple +1 -20
  50. package/tests/client/input-value.test.ripple +11 -31
  51. package/tests/client/map.test.ripple +3 -22
  52. package/tests/client/media-query.test.ripple +10 -23
  53. package/tests/client/object.test.ripple +5 -24
  54. package/tests/client/portal.test.ripple +2 -19
  55. package/tests/client/ref.test.ripple +8 -26
  56. package/tests/client/set.test.ripple +3 -22
  57. package/tests/client/svg.test.ripple +1 -22
  58. package/tests/client/switch.test.ripple +6 -25
  59. package/tests/client/tracked-expression.test.ripple +2 -21
  60. package/tests/client/typescript-generics.test.ripple +0 -21
  61. package/tests/client/url/url.derived.test.ripple +83 -0
  62. package/tests/client/url/url.parsing.test.ripple +165 -0
  63. package/tests/client/url/url.partial-removal.test.ripple +198 -0
  64. package/tests/client/url/url.reactivity.test.ripple +449 -0
  65. package/tests/client/url/url.serialization.test.ripple +50 -0
  66. package/tests/client/url-search-params/url-search-params.derived.test.ripple +84 -0
  67. package/tests/client/url-search-params/url-search-params.initialization.test.ripple +61 -0
  68. package/tests/client/url-search-params/url-search-params.iteration.test.ripple +153 -0
  69. package/tests/client/url-search-params/url-search-params.mutation.test.ripple +343 -0
  70. package/tests/client/url-search-params/url-search-params.retrieval.test.ripple +160 -0
  71. package/tests/client/url-search-params/url-search-params.serialization.test.ripple +53 -0
  72. package/tests/client/url-search-params/url-search-params.tracked-url.test.ripple +55 -0
  73. package/tests/client.d.ts +12 -0
  74. package/tests/server/if.test.ripple +66 -0
  75. package/tests/setup-client.js +28 -0
  76. package/tsconfig.json +4 -2
  77. package/types/index.d.ts +5 -1
  78. package/LICENSE +0 -21
  79. package/tests/client/__snapshots__/basic.test.ripple.snap +0 -117
  80. package/tests/client/__snapshots__/compiler.test.ripple.snap +0 -33
  81. package/tests/client/array.test.ripple +0 -1455
  82. package/tests/client/basic.test.ripple +0 -1892
  83. package/tests/client/compiler.test.ripple +0 -541
  84. package/tests/client/composite.test.ripple +0 -692
  85. package/tests/client/url-search-params.test.ripple +0 -912
  86. package/tests/client/url.test.ripple +0 -954
@@ -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
  }
@@ -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
+ });