Skip to content

Commit a122575

Browse files
committed
merge main
2 parents 310ac6b + 18dd456 commit a122575

File tree

17 files changed

+294
-46
lines changed

17 files changed

+294
-46
lines changed

.changeset/dirty-cycles-smash.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: send `$effect.pending` count to the correct boundary

.changeset/ninety-olives-report.md

Lines changed: 0 additions & 5 deletions
This file was deleted.

.changeset/wise-schools-report.md

Lines changed: 0 additions & 5 deletions
This file was deleted.

documentation/docs/07-misc/02-testing.md

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,9 @@ export function logger(getValue) {
160160

161161
### Component testing
162162

163-
It is possible to test your components in isolation using Vitest.
163+
It is possible to test your components in isolation, which allows you to render them in a browser (real or simulated), simulate behavior, and make assertions, without spinning up your whole app.
164164

165-
> [!NOTE] Before writing component tests, think about whether you actually need to test the component, or if it's more about the logic _inside_ the component. If so, consider extracting out that logic to test it in isolation, without the overhead of a component
165+
> [!NOTE] Before writing component tests, think about whether you actually need to test the component, or if it's more about the logic _inside_ the component. If so, consider extracting out that logic to test it in isolation, without the overhead of a component.
166166
167167
To get started, install jsdom (a library that shims DOM APIs):
168168

@@ -246,6 +246,48 @@ test('Component', async () => {
246246

247247
When writing component tests that involve two-way bindings, context or snippet props, it's best to create a wrapper component for your specific test and interact with that. `@testing-library/svelte` contains some [examples](https://testing-library.com/docs/svelte-testing-library/example).
248248

249+
### Component testing with Storybook
250+
251+
[Storybook](https://storybook.js.org) is a tool for developing and documenting UI components, and it can also be used to test your components. They're run with Vitest's browser mode, which renders your components in a real browser for the most realistic testing environment.
252+
253+
To get started, first install Storybook ([using Svelte's CLI](/docs/cli/storybook)) in your project via `npx sv add storybook` and choose the recommended configuration that includes testing features. If you're already using Storybook, and for more information on Storybook's testing capabilities, follow the [Storybook testing docs](https://storybook.js.org/docs/writing-tests?renderer=svelte) to get started.
254+
255+
You can create stories for component variations and test interactions with the [play function](https://storybook.js.org/docs/writing-tests/interaction-testing?renderer=svelte#writing-interaction-tests), which allows you to simulate behavior and make assertions using the Testing Library and Vitest APIs. Here's an example of two stories that can be tested, one that renders an empty LoginForm component and one that simulates a user filling out the form:
256+
257+
```svelte
258+
/// file: LoginForm.stories.svelte
259+
<script module>
260+
import { defineMeta } from '@storybook/addon-svelte-csf';
261+
import { expect, fn } from 'storybook/test';
262+
263+
import LoginForm from './LoginForm.svelte';
264+
265+
const { Story } = defineMeta({
266+
component: LoginForm,
267+
args: {
268+
// Pass a mock function to the `onSubmit` prop
269+
onSubmit: fn(),
270+
}
271+
});
272+
</script>
273+
274+
<Story name="Empty Form" />
275+
276+
<Story
277+
name="Filled Form"
278+
play={async ({ args, canvas, userEvent }) => {
279+
// Simulate a user filling out the form
280+
await userEvent.type(canvas.getByTestId('email'), '[email protected]');
281+
await userEvent.type(canvas.getByTestId('password'), 'a-random-password');
282+
await userEvent.click(canvas.getByRole('button'));
283+
284+
// Run assertions
285+
await expect(args.onSubmit).toHaveBeenCalledTimes(1);
286+
await expect(canvas.getByText('You’re in!')).toBeInTheDocument();
287+
}}
288+
/>
289+
```
290+
249291
## E2E tests using Playwright
250292

251293
E2E (short for 'end to end') tests allow you to test your full application through the eyes of the user. This section uses [Playwright](https://playwright.dev/) as an example, but you can also use other solutions like [Cypress](https://www.cypress.io/) or [NightwatchJS](https://nightwatchjs.org/).

packages/svelte/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# svelte
22

3+
## 5.38.7
4+
5+
### Patch Changes
6+
7+
- fix: replace `undefined` with `void(0)` in CallExpressions ([#16693](https://github.com/sveltejs/svelte/pull/16693))
8+
9+
- fix: ensure batch exists when resetting a failed boundary ([#16698](https://github.com/sveltejs/svelte/pull/16698))
10+
11+
- fix: place store setup inside async body ([#16687](https://github.com/sveltejs/svelte/pull/16687))
12+
313
## 5.38.6
414

515
### Patch Changes

packages/svelte/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "svelte",
33
"description": "Cybernetically enhanced web apps",
44
"license": "MIT",
5-
"version": "5.38.6",
5+
"version": "5.38.7",
66
"type": "module",
77
"types": "./types/index.d.ts",
88
"engines": {

packages/svelte/src/internal/client/dom/blocks/async.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
/** @import { TemplateNode, Value } from '#client' */
22
import { flatten } from '../../reactivity/async.js';
33
import { get } from '../../runtime.js';
4-
import { get_pending_boundary } from './boundary.js';
4+
import { get_boundary } from './boundary.js';
55

66
/**
77
* @param {TemplateNode} node
88
* @param {Array<() => Promise<any>>} expressions
99
* @param {(anchor: TemplateNode, ...deriveds: Value[]) => void} fn
1010
*/
1111
export function async(node, expressions, fn) {
12-
var boundary = get_pending_boundary();
12+
var boundary = get_boundary();
1313

1414
boundary.update_pending_count(1);
1515

packages/svelte/src/internal/client/dom/blocks/boundary.js

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,11 @@ export function boundary(node, props, children) {
4949
}
5050

5151
export class Boundary {
52-
pending = false;
53-
5452
/** @type {Boundary | null} */
5553
parent;
5654

55+
#pending = false;
56+
5757
/** @type {TemplateNode} */
5858
#anchor;
5959

@@ -81,6 +81,7 @@ export class Boundary {
8181
/** @type {DocumentFragment | null} */
8282
#offscreen_fragment = null;
8383

84+
#local_pending_count = 0;
8485
#pending_count = 0;
8586
#is_creating_fallback = false;
8687

@@ -95,12 +96,12 @@ export class Boundary {
9596

9697
#effect_pending_update = () => {
9798
if (this.#effect_pending) {
98-
internal_set(this.#effect_pending, this.#pending_count);
99+
internal_set(this.#effect_pending, this.#local_pending_count);
99100
}
100101
};
101102

102103
#effect_pending_subscriber = createSubscriber(() => {
103-
this.#effect_pending = source(this.#pending_count);
104+
this.#effect_pending = source(this.#local_pending_count);
104105

105106
if (DEV) {
106107
tag(this.#effect_pending, '$effect.pending()');
@@ -125,7 +126,7 @@ export class Boundary {
125126

126127
this.parent = /** @type {Effect} */ (active_effect).b;
127128

128-
this.pending = !!this.#props.pending;
129+
this.#pending = !!this.#props.pending;
129130

130131
this.#effect = block(() => {
131132
/** @type {Effect} */ (active_effect).b = this;
@@ -156,7 +157,7 @@ export class Boundary {
156157
this.#pending_effect = null;
157158
});
158159

159-
this.pending = false;
160+
this.#pending = false;
160161
}
161162
});
162163
} else {
@@ -169,7 +170,7 @@ export class Boundary {
169170
if (this.#pending_count > 0) {
170171
this.#show_pending_snippet();
171172
} else {
172-
this.pending = false;
173+
this.#pending = false;
173174
}
174175
}
175176
}, flags);
@@ -179,6 +180,14 @@ export class Boundary {
179180
}
180181
}
181182

183+
/**
184+
* Returns `true` if the effect exists inside a boundary whose pending snippet is shown
185+
* @returns {boolean}
186+
*/
187+
is_pending() {
188+
return this.#pending || (!!this.parent && this.parent.is_pending());
189+
}
190+
182191
has_pending_snippet() {
183192
return !!this.#props.pending;
184193
}
@@ -220,12 +229,25 @@ export class Boundary {
220229
}
221230
}
222231

223-
/** @param {1 | -1} d */
232+
/**
233+
* Updates the pending count associated with the currently visible pending snippet,
234+
* if any, such that we can replace the snippet with content once work is done
235+
* @param {1 | -1} d
236+
*/
224237
#update_pending_count(d) {
238+
if (!this.has_pending_snippet()) {
239+
if (this.parent) {
240+
this.parent.#update_pending_count(d);
241+
return;
242+
}
243+
244+
e.await_outside_boundary();
245+
}
246+
225247
this.#pending_count += d;
226248

227249
if (this.#pending_count === 0) {
228-
this.pending = false;
250+
this.#pending = false;
229251

230252
if (this.#pending_effect) {
231253
pause_effect(this.#pending_effect, () => {
@@ -240,14 +262,16 @@ export class Boundary {
240262
}
241263
}
242264

243-
/** @param {1 | -1} d */
265+
/**
266+
* Update the source that powers `$effect.pending()` inside this boundary,
267+
* and controls when the current `pending` snippet (if any) is removed.
268+
* Do not call from inside the class
269+
* @param {1 | -1} d
270+
*/
244271
update_pending_count(d) {
245-
if (this.has_pending_snippet()) {
246-
this.#update_pending_count(d);
247-
} else if (this.parent) {
248-
this.parent.#update_pending_count(d);
249-
}
272+
this.#update_pending_count(d);
250273

274+
this.#local_pending_count += d;
251275
effect_pending_updates.add(this.#effect_pending_update);
252276
}
253277

@@ -297,6 +321,9 @@ export class Boundary {
297321
e.svelte_boundary_reset_onerror();
298322
}
299323

324+
// If the failure happened while flushing effects, current_batch can be null
325+
Batch.ensure();
326+
300327
this.#pending_count = 0;
301328

302329
if (this.#failed_effect !== null) {
@@ -305,7 +332,7 @@ export class Boundary {
305332
});
306333
}
307334

308-
this.pending = true;
335+
this.#pending = true;
309336

310337
this.#main_effect = this.#run(() => {
311338
this.#is_creating_fallback = false;
@@ -315,7 +342,7 @@ export class Boundary {
315342
if (this.#pending_count > 0) {
316343
this.#show_pending_snippet();
317344
} else {
318-
this.pending = false;
345+
this.#pending = false;
319346
}
320347
};
321348

@@ -381,12 +408,8 @@ function move_effect(effect, fragment) {
381408
}
382409
}
383410

384-
export function get_pending_boundary() {
385-
var boundary = /** @type {Effect} */ (active_effect).b;
386-
387-
while (boundary !== null && !boundary.has_pending_snippet()) {
388-
boundary = boundary.parent;
389-
}
411+
export function get_boundary() {
412+
const boundary = /** @type {Effect} */ (active_effect).b;
390413

391414
if (boundary === null) {
392415
e.await_outside_boundary();

packages/svelte/src/internal/client/reactivity/async.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { DESTROYED } from '#client/constants';
44
import { DEV } from 'esm-env';
55
import { component_context, is_runes, set_component_context } from '../context.js';
6-
import { get_pending_boundary } from '../dom/blocks/boundary.js';
6+
import { get_boundary } from '../dom/blocks/boundary.js';
77
import { invoke_error_boundary } from '../error-handling.js';
88
import {
99
active_effect,
@@ -39,7 +39,7 @@ export function flatten(sync, async, fn) {
3939
var parent = /** @type {Effect} */ (active_effect);
4040

4141
var restore = capture();
42-
var boundary = get_pending_boundary();
42+
var boundary = get_boundary();
4343

4444
Promise.all(async.map((expression) => async_derived(expression)))
4545
.then((result) => {

packages/svelte/src/internal/client/reactivity/batch.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
} from '#client/constants';
1616
import { async_mode_flag } from '../../flags/index.js';
1717
import { deferred, define_property } from '../../shared/utils.js';
18-
import { get_pending_boundary } from '../dom/blocks/boundary.js';
18+
import { get_boundary } from '../dom/blocks/boundary.js';
1919
import {
2020
active_effect,
2121
is_dirty,
@@ -285,7 +285,10 @@ export class Batch {
285285
this.#render_effects.push(effect);
286286
} else if ((flags & CLEAN) === 0) {
287287
if ((flags & ASYNC) !== 0) {
288-
var effects = effect.b?.pending ? this.#boundary_async_effects : this.#async_effects;
288+
var effects = effect.b?.is_pending()
289+
? this.#boundary_async_effects
290+
: this.#async_effects;
291+
289292
effects.push(effect);
290293
} else if (is_dirty(effect)) {
291294
if ((effect.f & BLOCK_EFFECT) !== 0) this.#block_effects.push(effect);
@@ -697,12 +700,12 @@ export function schedule_effect(signal) {
697700
}
698701

699702
export function suspend() {
700-
var boundary = get_pending_boundary();
703+
var boundary = get_boundary();
701704
var batch = /** @type {Batch} */ (current_batch);
702705
// In case the pending snippet is shown, we want to update the UI immediately
703706
// and not have the batch be blocked on async work,
704707
// since the async work is happening "hidden" behind the pending snippet.
705-
var ignore_async = boundary.pending;
708+
var ignore_async = boundary.is_pending();
706709

707710
boundary.update_pending_count(1);
708711
if (!ignore_async) batch.increment();

0 commit comments

Comments
 (0)